Skip to content

Show one live question as a row in the chat - #936

Open
nortonandreev wants to merge 2 commits into
mainfrom
norton/ai-2765-chat-question-once-in-transcript
Open

nortonandreev wants to merge 2 commits into
mainfrom
norton/ai-2765-chat-question-once-in-transcript

Conversation

@nortonandreev

Copy link
Copy Markdown
Contributor

What & why

Hosted Claude can show the same AskUserQuestion twice — a local radio card and a server prompt-only card — in a docked pane off the transcript.

This shadows the server twin as soon as a local card exists, joining on session plus overlapping prompt text rather than waiting for ServerRequestId, and renders that one card as a virtualized chat row with the composer pinned.

Composer and Other… use the kit field chrome; the question header matches the tool-kind label; a card that follows its tool group drops the paragraph gap so the two read as one block.

Where to look

SameQuestionAs in PermissionService: a Bash permission and an elicitation in the same session must stay two cards.
Follow-tail in ChatTabView: a TextBox in the list is editing, so a click on Other… must not ScrollToEnd and recycle the row.

Verification

dotnet run --project test/Capacitor.App.Tests.Unit/Capacitor.App.Tests.Unit.csproj — 1935 succeeded, 0 failed.

Visuals

Before

Screenshot 2026-09-14 at 19 31 58

After

Screenshot 2026-09-14 at 20 11 01

@nortonandreev nortonandreev self-assigned this Sep 14, 2026
@linear-code

linear-code Bot commented Sep 14, 2026

Copy link
Copy Markdown

AI-2765

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Render deduplicated live questions in the chat transcript

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Deduplicates local and server question cards using session-scoped prompt fingerprints.
• Renders pending cards as virtualized transcript rows while keeping the composer pinned.
• Preserves card input focus and aligns question styling with shared field chrome.
Diagram

graph TD
  L["Local Question"] --> P["Permission Service"] --> C["Pending Cards"] --> S["Row Sync"] --> I["Chat Items"] --> V["Virtualized View"]
  R["Server Elicitation"] --> P
  T["Transcript Rows"] --> I
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Daemon-assigned correlation identifier
  • ➕ Provides deterministic correlation without comparing user-facing text
  • ➕ Avoids ambiguity when multiple identical questions exist in one session
  • ➖ Requires coordinated daemon and client protocol changes
  • ➖ Does not eliminate duplication until the identifier reaches the local request
2. View-model-only deduplication
  • ➕ Keeps matching logic close to the affected chat presentation
  • ➕ Avoids changing the shared permission cache
  • ➖ Other permission consumers could still display duplicate requests
  • ➖ Complicates resolution and disconnect resurfacing because both cache entries remain live

Recommendation: The PR’s service-level provisional join is the best near-term approach because it deduplicates every consumer, handles either arrival order, preserves the local card instance, and retains the server twin for disconnect recovery. A daemon-issued shared correlation identifier would be preferable long term, but prompt matching remains necessary until that identifier is available.

Files changed (12) +470 / -60

Enhancement (5) +138 / -42
App.axamlAdd shared field and tool-label styling +38/-3

Add shared field and tool-label styling

• Expands embedded TextBox styling to suppress Fluent chrome consistently and introduces reusable standalone field styling. Adds a shared tool-kind text style used by tool groups and question headers.

src/Capacitor.App/App.axaml

ChatItems.csIntroduce pending-card transcript rows +15/-1

Introduce pending-card transcript rows

• Adds PendingCardItem as a chat-row wrapper for live cards. Tool groups now expose packing state so an immediately following card can visually join the group.

src/Capacitor.App/ViewModels/ChatItems.cs

ChatTabViewModel.csSynchronize pending cards into the chat item list +38/-1

Synchronize pending cards into the chat item list

• Keeps pending cards at the end of the transcript item collection across replay, reset, and path changes. It also maintains tool-group packing state and suppresses empty-phase messaging when a card is present.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

ChatTabView.axamlRender pending cards inside the virtualized transcript +30/-23

Render pending cards inside the virtualized transcript

• Replaces the separate NEEDS YOU dock with PendingCardItem templates in the chat list. Adds contextual spacing for cards following tool groups and keeps queued messages and the composer pinned below the list.

src/Capacitor.App/Views/ChatTabView.axaml

PendingCardTemplates.axamlAlign pending cards with shared chat styling +17/-14

Align pending cards with shared chat styling

• Switches card outlines from status green to the standard border token and applies shared field chrome to free-text inputs. Question headers now use the tool-kind label style while prompts retain stronger typography.

src/Capacitor.App/Views/PendingCardTemplates.axaml

Bug fix (3) +59 / -0
PendingPermissionRequest.csMatch equivalent question requests by prompt fingerprint +21/-0

Match equivalent question requests by prompt fingerprint

• Adds session-scoped comparison for local questions and server elicitations. Fingerprints cover plain prompts and header-plus-question formatting while excluding ordinary permission requests.

src/Capacitor.App/Services/PendingPermissionRequest.cs

PermissionService.csShadow duplicate local and server questions immediately +25/-0

Shadow duplicate local and server questions immediately

• Correlates matching question requests before ServerRequestId is supplied, regardless of arrival order. The local request retains its instance while the server twin is stored for lifecycle recovery.

src/Capacitor.App/Services/PermissionService.cs

ChatTabView.axaml.csPreserve focus for inputs inside virtualized rows +13/-0

Preserve focus for inputs inside virtualized rows

• Ignores reader gestures and follow-tail corrections while a TextBox within the chat list owns focus. This prevents row recycling from dropping the caret during Other or free-text editing.

src/Capacitor.App/Views/ChatTabView.axaml.cs

Refactor (1) +0 / -9
SignInStepView.axamlUse the application-wide field style +0/-9

Use the application-wide field style

• Removes the onboarding-local kcapField definition now that equivalent field chrome is provided globally.

src/Capacitor.App/Views/Onboarding/SignInStepView.axaml

Tests (3) +273 / -9
ChatTabViewModelTests.csCover pending-card row synchronization and packing +65/-0

Cover pending-card row synchronization and packing

• Verifies that cards appear at the end of chat items, survive path switches, update on removal, and pack only when following tool groups. Also confirms card-only states suppress the empty transcript note.

test/Capacitor.App.Tests.Unit/ChatTabViewModelTests.cs

ChatTabViewSmokeTests.csValidate virtualized card rendering, focus, and styling +117/-9

Validate virtualized card rendering, focus, and styling

• Updates rendering assertions for transcript-hosted cards and adds coverage for spacing, header typography, field chrome, and focused Other input behavior under virtualization.

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs

PermissionServiceTests.csCover prompt-based question reconciliation +91/-0

Cover prompt-based question reconciliation

• Tests both request arrival orders, session and prompt mismatch safeguards, header-inclusive fingerprints, permission exclusion, and resurfacing of a shadowed server question after disconnect.

test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Repeated prompts hide live requests 🔗 Cross-repo conflict ≡ Correctness
Description
SameQuestionAs, ShadowMatchingServerQuestions, and LocalQuestionClaimant use same-session
first-prompt text as request identity, allowing one local card to remove multiple server
elicitations while ShadowTwin retains only the first ServerRequestId. When distinct concurrent
requests share that prompt, settling the visible card concludes only its recorded ID and leaves the
other request pending but inaccessible in _shadowed until the local lane disconnects.
Code

src/Capacitor.App/Services/PermissionService.cs[R265-266]

+        foreach (var twin in _cache.Items.Where(i => i.Lane == PermissionLane.Server && local.SameQuestionAs(i)).ToList())
+            ShadowTwin(local, twin);
Relevance

●●● Strong

Concrete concurrency bug can hide distinct pending requests; accepted history favors preserving
per-request state.

PR-#831
PR-#889

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The fallback predicate compares only the session and overlapping first-question fingerprints, with
no unique request identity or cardinality check, and the local upsert loop removes every matching
server item. ShadowTwin stores only the first server request ID and ConcludeLocal concludes only
that ID, while kcap-server supports multiple pending interactions per session, tracks each under a
distinct request ID, and completes exactly the specified ID; therefore, additional same-prompt
requests remain hidden in _shadowed.

src/Capacitor.App/Services/PendingPermissionRequest.cs[103-109]
src/Capacitor.App/Services/PermissionService.cs[262-276]
src/Capacitor.App/Services/PermissionService.cs[304-321]
src/Capacitor.App/Services/PendingPermissionRequest.cs[105-120]
src/Capacitor.App/Services/PermissionService.cs[263-276]
src/Capacitor.Cli.Core/ClaudeElicitation.cs[41-67]
src/Capacitor.App/Services/PendingPermissionRequest.cs[103-121]
src/Capacitor.App/Services/PermissionService.cs[262-283]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/AcpInteractionTracker.cs [42-49]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/AcpInteractionTracker.cs [67-79]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/AcpInteractionTracker.cs [82-110]

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

## Issue description
Prompt equality is not a unique cross-repository identity. Multiple kcap-server interactions in one session may have identical prompts and distinct request IDs, but the current reconciliation can shadow all of them behind one local card while retaining and concluding only one ID.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[262-283]
- src/Capacitor.App/Services/PendingPermissionRequest.cs[103-122]

## Recommended Fix
Make heuristic prompt-based reconciliation strictly one-to-one. Only consider unmatched local and server questions, never prompt-match a local card that already has a different `ServerRequestId`, and shadow only when exactly one unmatched local candidate and one unmatched server candidate share the fingerprint; stop after assigning one server request to one local request. Preserve exact identifier-based matching for confirmed daemon mappings, leave ambiguous same-prompt requests visible until authoritative request-ID correlation is available, and add tests for repeated concurrent prompts arriving in both orders.

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



Remediation recommended

2. Old prompts return after reconnecting 🐞 Bug ≡ Correctness
Description
ShadowTwin writes a heuristic server identifier into the local request, but the later exact daemon
mapping overwrites it without releasing the previously shadowed server item. If the text join
initially selects the wrong same-prompt request, that request remains hidden and is restored as
pending when the local subscription reconnects.
Code

src/Capacitor.App/Services/PermissionService.cs[R271-273]

+        if (local.ServerRequestId is null) {
+            local.ServerRequestId = twin.RequestId;
+            _cache.Refresh(local);
Relevance

●●● Strong

Stale shadow cleanup during identifier remapping is a concrete reconnect correctness issue.

PR-#831

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new prompt join mutates ServerRequestId and stores the selected twin in _shadowed.
UpsertLocal can later replace that identifier and shadows only the new value, while
DropLocalLane restores every leftover shadow, causing the superseded request to reappear.

src/Capacitor.App/Services/PermissionService.cs[230-248]
src/Capacitor.App/Services/PermissionService.cs[269-276]
src/Capacitor.App/Services/PermissionService.cs[285-295]
src/Capacitor.Cli.Daemon/Services/PermissionPromptBroker.cs[61-69]

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

## Issue description
A prompt-derived server mapping can be superseded by the daemon's exact mapping while its old server request remains indefinitely in the shadow dictionary.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[230-248]
- src/Capacitor.App/Services/PermissionService.cs[269-276]
- src/Capacitor.App/Services/PermissionService.cs[285-295]

## Recommended Fix
Track whether a local mapping was inferred from prompt text. Before replacing an inferred identifier with a different daemon-provided identifier, remove the old twin from `_shadowed` and restore it to the cache when it is still pending and not tombstoned, then shadow only the confirmed identifier.

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


3. Earlier tool groups appear attached ✓ Resolved 🐞 Bug ≡ Correctness
Description
SyncPendingCardItems clears PacksWithCard only on the item immediately before the currently
trailing card segment, not on the group that previously owned the card. When transcript rows are
appended after an existing card, the old group retains its reduced bottom margin even though the
card moves to the new end of the list.
Code

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[R590-591]

+        if (start > 0 && _items[start - 1] is ToolGroupItem previous)
+            previous.PacksWithCard = false;
Relevance

●●● Strong

Persistent stale layout state after list synchronization matches recent accepted stale-projection
fixes.

PR-#889

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Fresh transcript rows are appended before synchronization, so an existing card may no longer be
trailing when the method begins. The cleanup then examines the new final transcript row rather than
the previously packed group, and the persistent flag continues driving the reduced-margin style.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[575-603]
src/Capacitor.App/ViewModels/ChatItems.cs[146-152]
src/Capacitor.App/Views/ChatTabView.axaml[8-18]
src/Capacitor.App/Views/ChatTabView.axaml[90-97]

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

## Issue description
Moving pending cards after newly appended transcript rows leaves the previously packed tool group marked as adjacent to a card.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[582-603]
- src/Capacitor.App/ViewModels/ChatItems.cs[146-152]

## Recommended Fix
Before removing or repositioning pending-card rows, clear `PacksWithCard` on every tool group currently marked as packed, or derive the flag from final row adjacency. After rebuilding the trailing cards, set it only on the final tool group when that group directly precedes the first card.

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


4. Failed smoke checks leave test windows open 🐞 Bug ☼ Reliability
Description
The newly added smoke tests create a Host and invoke CloseAsync only after all assertions
succeed, rather than in a finally block. If a wait or assertion throws, the Avalonia window plus
chat and terminal subscriptions survive into subsequent tests in the process-global UI session.
Code

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[R986-989]

+    public async Task A_question_header_paints_like_the_tool_kind_chip() {
+        await RunOnUiAsync(async () => {
+            var host = new Host();
+            await host.LoadAsync(Tmp.CreateFile("ask.jsonl", [
Relevance

●●● Strong

Directly matches the team’s recent accepted Avalonia smoke-test cleanup precedent.

PR-#858

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Host.CloseAsync explicitly closes the window and tears down both view models, but the new tests
only reach it as their final normal-path statement. This matches the previously accepted
failure-path cleanup issue in headless Avalonia smoke tests.

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[986-1009]
test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[1014-1089]
test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[190-195]
PR-#858

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

Issue description
The new headless UI tests close their hosts only on the success path. A failed assertion before that call leaves the window and view-model subscriptions alive, contaminating the shared Avalonia test session.

Fix Focus Areas
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[986-1089]

Recommended Fix
Wrap each newly added `Host` lifecycle in `try`/`finally` and call `await host.CloseAsync()` in the `finally` block. Apply this to every new test that creates a host, so cleanup occurs after failures as well as successful assertions.

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



Informational

5. New chat row lacks its own file 📘 Rule violation ⚙ Maintainability
Description
ChatItems.cs adds the public top-level PendingCardItem beside several existing public chat row
types instead of declaring it in PendingCardItem.cs. Because views and tests consume this type
outside the file, it does not qualify for the private/internal hierarchy exception, so
filename-based discovery and ownership become ambiguous for later changes.
Code

src/Capacitor.App/ViewModels/ChatItems.cs[207]

+public sealed class PendingCardItem(PendingCardViewModel card, bool packsWithPrevious = false) : ChatItemViewModel {
Relevance

● Weak

Recent precedent rejected equivalent one-type-per-file naming enforcement for partial view-model
files.

PR-#865

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3162234 requires one primary non-private type per source file, with the filename matching that
type; related hierarchy implementations are exempt only when private or internal. The added
declaration is public and joins multiple existing public top-level chat item types in
ChatItems.cs.

Rule 3162234: One primary type per file, with only narrow documented exceptions
src/Capacitor.App/ViewModels/ChatItems.cs[8-26]
src/Capacitor.App/ViewModels/ChatItems.cs[88-92]
src/Capacitor.App/ViewModels/ChatItems.cs[206-210]

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

## Issue description
`PendingCardItem` is a public top-level type added to `ChatItems.cs`, which already contains several public types and does not match the new type's name.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatItems.cs[206-210]

## Recommended Fix
Move `PendingCardItem` and its documentation into a new `src/Capacitor.App/ViewModels/PendingCardItem.cs` file, preserving its namespace, visibility, constructor, and properties.

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


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: 87c1dcd4)
Review mode: 🧠 Deep: This is a behavior-heavy, cross-cutting change spanning permission reconciliation, virtualized chat item synchronization, Avalonia focus/scroll behavior, styling, and numerous independent UI and service paths where multiple subtle defects could be missed in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +271 to +273
if (local.ServerRequestId is null) {
local.ServerRequestId = twin.RequestId;
_cache.Refresh(local);

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. Old prompts return after reconnecting 🐞 Bug ≡ Correctness

ShadowTwin writes a heuristic server identifier into the local request, but the later exact daemon
mapping overwrites it without releasing the previously shadowed server item. If the text join
initially selects the wrong same-prompt request, that request remains hidden and is restored as
pending when the local subscription reconnects.
Agent Prompt
## Issue description
A prompt-derived server mapping can be superseded by the daemon's exact mapping while its old server request remains indefinitely in the shadow dictionary.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[230-248]
- src/Capacitor.App/Services/PermissionService.cs[269-276]
- src/Capacitor.App/Services/PermissionService.cs[285-295]

## Recommended Fix
Track whether a local mapping was inferred from prompt text. Before replacing an inferred identifier with a different daemon-provided identifier, remove the old twin from `_shadowed` and restore it to the cache when it is still pending and not tombstoned, then shadow only the confirmed identifier.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same case as the prompt-join thread: overwriting ServerRequestId with a different id only happens if the text join already picked the wrong twin. One local card plus one server elicitation is a no-op overwrite; a leftover shadow does not arise on reconnect.

Comment thread src/Capacitor.App/ViewModels/ChatTabViewModel.cs Outdated
Comment on lines +986 to +989
public async Task A_question_header_paints_like_the_tool_kind_chip() {
await RunOnUiAsync(async () => {
var host = new Host();
await host.LoadAsync(Tmp.CreateFile("ask.jsonl", [

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

4. Failed smoke checks leave test windows open 🐞 Bug ☼ Reliability

The newly added smoke tests create a Host and invoke CloseAsync only after all assertions
succeed, rather than in a finally block. If a wait or assertion throws, the Avalonia window plus
chat and terminal subscriptions survive into subsequent tests in the process-global UI session.
Agent Prompt
Issue description
The new headless UI tests close their hosts only on the success path. A failed assertion before that call leaves the window and view-model subscriptions alive, contaminating the shared Avalonia test session.

Fix Focus Areas
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[986-1089]

Recommended Fix
Wrap each newly added `Host` lifecycle in `try`/`finally` and call `await host.CloseAsync()` in the `finally` block. Apply this to every new test that creates a host, so cleanup occurs after failures as well as successful assertions.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same CloseAsync-after-assert pattern as the rest of ChatTabViewSmokeTests. These tests already share the Avalonia session lock; wrapping only the new ones would not change the suite contract.

Comment on lines +265 to +266
foreach (var twin in _cache.Items.Where(i => i.Lane == PermissionLane.Server && local.SameQuestionAs(i)).ToList())
ShadowTwin(local, twin);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Repeated prompts hide live requests 🔗 Cross-repo conflict ≡ Correctness

SameQuestionAs, ShadowMatchingServerQuestions, and LocalQuestionClaimant use same-session
first-prompt text as request identity, allowing one local card to remove multiple server
elicitations while ShadowTwin retains only the first ServerRequestId. When distinct concurrent
requests share that prompt, settling the visible card concludes only its recorded ID and leaves the
other request pending but inaccessible in _shadowed until the local lane disconnects.
Agent Prompt
## Issue description
Prompt equality is not a unique cross-repository identity. Multiple kcap-server interactions in one session may have identical prompts and distinct request IDs, but the current reconciliation can shadow all of them behind one local card while retaining and concluding only one ID.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[262-283]
- src/Capacitor.App/Services/PendingPermissionRequest.cs[103-122]

## Recommended Fix
Make heuristic prompt-based reconciliation strictly one-to-one. Only consider unmatched local and server questions, never prompt-match a local card that already has a different `ServerRequestId`, and shadow only when exactly one unmatched local candidate and one unmatched server candidate share the fingerprint; stop after assigning one server request to one local request. Preserve exact identifier-based matching for confirmed daemon mappings, leave ambiguous same-prompt requests visible until authoritative request-ID correlation is available, and add tests for repeated concurrent prompts arriving in both orders.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Prompt-text join is the correlation we have until the daemon writes ServerRequestId. Hosted Claude has one live AskUserQuestion; a Bash permission in the same session does not match. Two distinct concurrent questions with the same first-prompt text is not a path here, and requiring a unique unmatched pair would leave a duplicate hub replay as a second card.

@nortonandreev
nortonandreev force-pushed the norton/ai-2765-chat-question-once-in-transcript branch from 51a94a4 to 52d2d24 Compare September 14, 2026 14:23
@nortonandreev

Copy link
Copy Markdown
Contributor Author

On the informational finding: PendingCardItem belongs with the other public chat row types in ChatItems.cs (UserTurnItem, ToolGroupItem, and the rest). Splitting it out would not match how this file is owned.

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.

1 participant