Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b20448f
Fix #1618: switch entity Views to nonvirtualized panel below 100 nodes
JoshuaRowePhantom Oct 8, 2026
50ba655
Fix #1618: retain visible tree anchors and focused card controls
JoshuaRowePhantom Oct 8, 2026
5ba0c42
Fix #1618: cancel stale tree transitions and restore editor focus
JoshuaRowePhantom Oct 8, 2026
b8b1adc
Fix mixed external and note card presentation (#1623)
JoshuaRowePhantom Oct 8, 2026
91c4a68
Fix #1621: route external card URLs through in-app opener
JoshuaRowePhantom Oct 8, 2026
11c7037
Fix populated view tabs independent of left navigation
JoshuaRowePhantom Oct 8, 2026
26ac096
Clean Copilot wrapper prerequisite cache with project Clean
JoshuaRowePhantom Oct 8, 2026
48020b5
Test mixed external-note tab rendering across reopen (#1623)
JoshuaRowePhantom Oct 8, 2026
05806a3
Verify external card wiring and surface launcher failures
JoshuaRowePhantom Oct 8, 2026
859e40b
Test successful external URI launch skips shell fallback
JoshuaRowePhantom Oct 8, 2026
2676f58
Merge fix/1621-external-card-url into integration/1621-1623
Copilot Oct 8, 2026
40d2bb2
Finish populated view tab isolation and acceptance coverage
JoshuaRowePhantom Oct 8, 2026
544d5c9
Merge fix/1623-mixed-note-external into integration/1621-1623
Copilot Oct 9, 2026
b6a49cc
Keep Copilot prerequisite leased through test and publish lifetimes
JoshuaRowePhantom Oct 9, 2026
e94916a
Fix #1624: make rendered entity-card text selectable and copyable
JoshuaRowePhantom Oct 9, 2026
c06e06a
Merge fix/1620-clean-wrapper-cache into integration/release-1620-1623
JoshuaRowePhantom Oct 9, 2026
e49a3b8
Merge fix/1622-populated-view-tab into integration/release-1620-1623
JoshuaRowePhantom Oct 9, 2026
406332b
Merge integration/1621-1623 into integration/release-1620-1623
JoshuaRowePhantom Oct 9, 2026
b7482cc
Follow up #1624: copy action labels and exercise card interactions
JoshuaRowePhantom Oct 9, 2026
f2ef9d3
Merge fix/1624-copyable-card-text into integration/release-1620-1623
JoshuaRowePhantom Oct 9, 2026
41eb62e
Restore unavailable link affordance without blocking text selection
JoshuaRowePhantom Oct 9, 2026
6367295
Merge fix/1619-idle-subagents into integration/release-1620-1623
JoshuaRowePhantom Oct 9, 2026
e525b37
Allow cold prerequisite build in hosted fast CI
JoshuaRowePhantom Oct 9, 2026
d2056a6
Fix hosted full CI Copilot CLI and MongoDB mount prerequisites
JoshuaRowePhantom Oct 9, 2026
1ea3ccb
Allow cold Atlas Local startup in hosted full CI
JoshuaRowePhantom Oct 9, 2026
ddb6fa9
Capture MongoDB container state when hosted tests fail
JoshuaRowePhantom Oct 9, 2026
f700f62
Grant hosted Atlas Local access to test bind mounts
JoshuaRowePhantom Oct 9, 2026
d46f733
Prepare direct replica-set test bind mount on Linux
JoshuaRowePhantom Oct 9, 2026
6ad2581
Serialize hosted Windows test hosts and await WebView DOM updates
JoshuaRowePhantom Oct 9, 2026
c0220e5
Script completed subagent notification in live WebView test
JoshuaRowePhantom Oct 9, 2026
92ee5f7
Merge validated #1616 into release integration
JoshuaRowePhantom Oct 9, 2026
eb077a7
Wait for remote queue revision before second acceptance command
JoshuaRowePhantom Oct 9, 2026
b61060f
Merge remote-tracking branch 'github/fix/hosted-full-ci-prerequisites…
JoshuaRowePhantom Oct 9, 2026
c66a3f5
Fix #1615 and #1617: preserve tool-run narration and notification chr…
JoshuaRowePhantom Oct 7, 2026
bf7f591
Fix #1615 and #1617: preserve narration order and test remote WebView…
JoshuaRowePhantom Oct 7, 2026
c3f2394
Fix #1615 and #1617: keep unmatched results ordered and bridge remote…
JoshuaRowePhantom Oct 7, 2026
287e4d4
Fix #1615: route all same-run group actions to visible chrome
JoshuaRowePhantom Oct 8, 2026
2d49820
Align chat affordance contracts with run-level routing
JoshuaRowePhantom Oct 8, 2026
fecf158
Reconcile remote child navigation with browser display updates
JoshuaRowePhantom Oct 9, 2026
d04986f
Merge verified #1615 renderer integration
JoshuaRowePhantom Oct 9, 2026
766eff6
Keep CI test progress and allow complete fast gate
JoshuaRowePhantom Oct 9, 2026
e8685f4
Synchronize WebView inspect action assertions with host bridge
JoshuaRowePhantom Oct 9, 2026
546884d
Merge synchronized #1615 WebView host assertions
JoshuaRowePhantom Oct 9, 2026
a638920
Fix desktop entity-tree anchor and focus during panel transitions
JoshuaRowePhantom Oct 9, 2026
526840b
Preserve hosted full-suite output on timeout
JoshuaRowePhantom Oct 9, 2026
d08c38b
Integrate #1618 entity-tree panel fix into release candidate
JoshuaRowePhantom Oct 9, 2026
49216aa
Integrate #1618 entity-tree panel transition fix
JoshuaRowePhantom Oct 9, 2026
e255d73
Fix VS Code tunnel poll test deadlock on Windows
JoshuaRowePhantom Oct 9, 2026
513a980
Merge deterministic VS Code tunnel poll regression
JoshuaRowePhantom Oct 9, 2026
535e363
Test immediate entity-tree anchor and focus across panel threshold
JoshuaRowePhantom Oct 9, 2026
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
63 changes: 61 additions & 2 deletions .github/workflows/ci-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ jobs:
shell: pwsh
run: dotnet build Phantom.Workspaces.slnx -c Release --no-restore

# The SDK-staged copilot.exe is a server-only runtime (it rejects --version).
# BYOK tests need the full standalone CLI, pinned to the SDK's CLI version.
- name: Install Copilot CLI for BYOK tests
shell: pwsh
run: |
$version = '1.0.83' # GitHub.Copilot.SDK 1.0.13's CopilotCliVersion
$expectedSha256 = '0e07221a275fdf7e61619c53566e3a421fd646d74d8e9ca491dbbff221f22945'
$directory = Join-Path $PWD '.ci-tools\copilot'
New-Item -ItemType Directory -Path $directory -Force | Out-Null
$archive = Join-Path $directory 'copilot.zip'
Invoke-WebRequest "https://github.com/github/copilot-cli/releases/download/v$version/copilot-win32-x64.zip" -OutFile $archive
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $expectedSha256) {
throw "Copilot CLI archive hash mismatch"
}
Expand-Archive -LiteralPath $archive -DestinationPath $directory
$cli = Join-Path $directory 'copilot.exe'
if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) {
throw "Standalone Copilot CLI not found: $cli"
}
& $cli --version
if ($LASTEXITCODE -ne 0) {
throw "Standalone Copilot CLI failed its version check (exit $LASTEXITCODE)"
}
"COPILOT_CLI_PATH=$cli" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

# windows-latest cannot start the Linux MongoDB container, so exclude the
# SlowDocker tests here; they run in the ubuntu-latest job below. Also exclude
# RequiresLocalConsole: the hosted windows-latest ConPTY renders no cmd.exe/pwsh
Expand All @@ -47,7 +72,12 @@ jobs:
shell: pwsh
run: |
dotnet test Phantom.Workspaces.slnx --no-restore --nologo -v minimal `
--filter "Category!=SlowDocker & Category!=RequiresLocalConsole"
-maxcpucount:1 `
--filter "Category!=SlowDocker & Category!=RequiresLocalConsole" 2>&1 |
Tee-Object -FilePath scripts/test-results.log
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

- name: Upload Windows test logs
if: always()
Expand All @@ -73,11 +103,40 @@ jobs:
- name: Set up build
uses: ./.github/actions/setup-build

# Pull before the test host starts: Atlas Local's cold image download can exceed the
# test harness's per-test inactivity watchdog on a fresh hosted runner.
- name: Pull MongoDB Atlas Local image
run: docker pull mongodb/mongodb-atlas-local:latest

# The image runs as a non-root user. New bind directories created by .NET under
# the runner's default TMPDIR are owned by the runner and are not writable by MongoDB.
# Default ACLs also cover the unique directories created by the replica-set tests.
- name: Prepare writable MongoDB test mounts
shell: bash
run: |
if ! command -v setfacl >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y acl
fi
mongo_uid=$(docker run --rm --entrypoint id mongodb/mongodb-atlas-local:latest -u)
directory="$GITHUB_WORKSPACE/.ci-tools/mongodb-tmp"
mkdir -p "$directory"
setfacl -m "u:$mongo_uid:rwx,d:u:$mongo_uid:rwx" "$directory"
echo "TMPDIR=$directory" >> "$GITHUB_ENV"

# Only the platform-agnostic data-layer Docker/Git integration tests run on
# Linux; the Windows-only GUI/scheduled-task/junction tests run above.
- name: Run Docker + Git integration tests
shell: pwsh
run: .\scripts\run-tests.ps1 -Mode full -TestNames Phantom.Workspaces.Data.MongoDB.Tests
run: .\scripts\run-tests.ps1 -Mode full -TestNames Phantom.Workspaces.Data.MongoDB.Tests -PerTestHangTimeout 10m

- name: Diagnose MongoDB container on failure
if: failure()
run: |
docker ps -a --filter name=pw-mongodb-test
docker inspect pw-mongodb-test --format '{{json .State}}' || true
docker logs --tail 150 pw-mongodb-test 2>&1 || true
free -h

- name: Upload Linux test logs
if: always()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
build-and-test:
name: Build and fast tests (windows-latest)
runs-on: windows-latest
timeout-minutes: 10
timeout-minutes: 120
env:
AVALONIA_UI_LICENSE_KEY: ${{ secrets.AVALONIA_UI_LICENSE_KEY }}
steps:
Expand All @@ -37,7 +37,7 @@ jobs:
shell: pwsh
run: |
Write-Host 'Fast filter: Category!=SlowDocker & Category!=SlowGit & Category!=Integration & Category!=WebView & Category!=SlowLayout & Category!=RequiresLocalConsole'
.\scripts\run-tests.ps1 -Mode fast
.\scripts\run-tests.ps1 -Mode fast -PerTestHangTimeout 10m

- name: Upload test results log
if: always()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ public void ChatOutput_SuppressedAssistantHeaderWithInspectTargets_RemainsHidden
{
var html = ReadShellHtml();
Assert.DoesNotContain("header.hidden = false", html, StringComparison.Ordinal);
Assert.Contains("header.hidden ? message.querySelector(\":scope > .chat-contents\") : meta", html, StringComparison.Ordinal);
Assert.Contains("var host = header.hidden", html, StringComparison.Ordinal);
Assert.Contains("message.querySelector(\":scope > .chat-contents > .chat-tool-group-actions\")", html, StringComparison.Ordinal);
Assert.Contains("message.querySelector(\":scope > .chat-contents\")", html, StringComparison.Ordinal);
Assert.Contains("postToHost({", html, StringComparison.Ordinal);
Assert.Contains("type: \"inspect\"", html, StringComparison.Ordinal);
Assert.Contains("CopyGutter.init(document)", html, StringComparison.Ordinal);
Expand All @@ -98,8 +100,11 @@ public void ChatOutputShellHtml_UsageInspectAffordance_DeduplicatesPerMessage()
{
var html = ReadShellHtml();

Assert.Contains("meta.querySelector(\".usage-gutter-btn\")", html, StringComparison.Ordinal);
Assert.DoesNotContain("previousElementSibling", html, StringComparison.Ordinal);
Assert.Contains("var leader = runId && Array.from(document.querySelectorAll(\"details.chat-tool-group[data-assistant-run-id]\"))", html, StringComparison.Ordinal);
Assert.Contains("var host = leader && leader.previousElementSibling;", html, StringComparison.Ordinal);
Assert.Contains("var ids = new Set(targets.map(function (target) { return target.id; }));", html, StringComparison.Ordinal);
Assert.Contains("if (!ids.has(button.getAttribute(\"data-segment-target-id\"))) { button.remove(); }", html, StringComparison.Ordinal);
Assert.Contains("if (button.parentElement !== host) { button.remove(); }", html, StringComparison.Ordinal);
}

[Fact]
Expand Down
144 changes: 123 additions & 21 deletions Phantom.Workspaces.Agent.Gui.Tests/ChatOutputHtmlModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1617,7 +1617,7 @@ public void GenerateHistoryChunk_DoesNotInvokeOnInsertOrEmitSinkCommands()
}

[Fact]
public void BuildHistoryRenderPlan_ResultInNewerChunkCallInOlderChunk_InjectedIntoCall()
public void BuildHistoryRenderPlan_ResultInNewerChunkAfterNarration_RemainsInNewerChunk()
{
// The call sits at index 10 (older chunk) while its result sits at index 300 (newer chunk).
var snapshot = Enumerable.Range(0, 10)
Expand All @@ -1636,20 +1636,17 @@ public void BuildHistoryRenderPlan_ResultInNewerChunkCallInOlderChunk_InjectedIn

var plan = BuildPlan(snapshot, sink);

// The result-only message renders no element of its own.
Assert.False(plan.Slots[300].HasDomElement);
Assert.True(plan.Slots[300].HasDomElement);

// The older chunk containing the call renders the injected result nested under it.
Assert.True(plan.Chunks.Count >= 2);
var olderChunk = plan.Chunks.First(chunk => chunk.Start <= 10 && chunk.End > 10);
var html = ChatOutputHtmlModel.GenerateHistoryChunk(plan, olderChunk.Start, olderChunk.End);
Assert.Contains("cross-chunk result data", html);
Assert.Contains("chat-tool-result", html);
Assert.DoesNotContain("cross-chunk result data", html);

// The newer chunk contains no standalone rendering of the result message.
var newerChunk = plan.Chunks.First(chunk => chunk.Start <= 300 && chunk.End > 300);
var newerHtml = ChatOutputHtmlModel.GenerateHistoryChunk(plan, newerChunk.Start, newerChunk.End);
Assert.DoesNotContain($"id=\"{ChatOutputHtmlRenderer.MessageId(300)}\"", newerHtml);
Assert.Contains($"id=\"{ChatOutputHtmlRenderer.MessageId(300)}\"", newerHtml);
Assert.Contains("cross-chunk result data", newerHtml);
}

[Fact]
Expand Down Expand Up @@ -2706,7 +2703,7 @@ public async Task Regression_BugD_OnInsert_UsesContainerPath_NotLoadAfterHardcod
}

[AvaloniaFact(Timeout = 15_000)]
public async Task Regression_BugE_CrossChunkToolResult_MatchesCall()
public async Task Regression_BugE_CrossChunkToolResult_AfterNarrationRendersAtEncounterPosition()
{
// The call lands in the older chunk while its result lands in the newer chunk.
var history = new ObservableCollection<AgentChatHistoryItem>(
Expand All @@ -2733,14 +2730,11 @@ public async Task Regression_BugE_CrossChunkToolResult_MatchesCall()

var allContent = string.Concat(sink.ContentOperations.Select(op => op.Content));

// The result is injected (nested under its call), never rendered standalone.
Assert.Contains("cross-chunk result data", allContent);
Assert.DoesNotContain($"id=\"{ChatOutputHtmlRenderer.MessageId(300)}\"", allContent);
Assert.Contains($"id=\"{ChatOutputHtmlRenderer.MessageId(300)}\"", allContent);

// The chunk containing the call renders it with the injected result.
var chunkWithCall = sink.ContentOperations.First(op => op.Content.Contains("my_tool"));
Assert.Contains("cross-chunk result data", chunkWithCall.Content);
Assert.Contains("chat-tool-result", chunkWithCall.Content);
Assert.DoesNotContain("cross-chunk result data", chunkWithCall.Content);
}

// ── Live insertion anchor regression tests (issue #900) ────────────────────
Expand Down Expand Up @@ -2909,16 +2903,17 @@ public async Task ChatOutput_CompletedTurnWithInterleavedAssistantContent_Coales
using var model = new ChatOutputHtmlModel(history, new ObservableCollection<AgentChatRunningItem>(), () => true, sink);
await model.HistoryLoaded;
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
Assert.Single(System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Cast<object>());
Assert.Contains("2 calls", html);
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Count);
Assert.Contains("id=\"tool-group-0-header\" hidden", html);
var read = html.IndexOf("read_file(…)", StringComparison.Ordinal);
var between = html.IndexOf("between calls", StringComparison.Ordinal);
var write = html.IndexOf("write_file(…)", StringComparison.Ordinal);
var finished = html.IndexOf("finished", StringComparison.Ordinal);
Assert.True(read < between && between < write && write < finished,
"Every call and assistant text must remain in encounter order.");
Assert.Contains("class=\"chat-content chat-tool-group\" open", html);
Assert.DoesNotContain("between calls", html.Substring(html.IndexOf("id=\"tool-group-0-details\"", StringComparison.Ordinal),
html.IndexOf("</details>", html.IndexOf("id=\"tool-group-0-details\"", StringComparison.Ordinal), StringComparison.Ordinal)
- html.IndexOf("id=\"tool-group-0-details\"", StringComparison.Ordinal)));
}

[AvaloniaFact(Timeout = 15_000)]
Expand All @@ -2939,13 +2934,121 @@ public async Task ChatOutput_SameRunMixedContent_GroupsAllCallsWithoutLosingText
using var model = new ChatOutputHtmlModel(history, new ObservableCollection<AgentChatRunningItem>(), () => true, sink);
await model.HistoryLoaded;
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
Assert.Single(System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Cast<object>());
Assert.Contains("2 calls", html);
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Count);
Assert.True(html.IndexOf("before call", StringComparison.Ordinal) < html.IndexOf("read_file(…)", StringComparison.Ordinal));
Assert.True(html.IndexOf("read_file(…)", StringComparison.Ordinal) < html.IndexOf("between calls", StringComparison.Ordinal));
Assert.True(html.IndexOf("between calls", StringComparison.Ordinal) < html.IndexOf("write_file(…)", StringComparison.Ordinal));
Assert.Contains("result-one", html);
Assert.Contains("result-two", html);
Assert.True(html.IndexOf("before call", StringComparison.Ordinal) <
html.IndexOf("read_file(…)", StringComparison.Ordinal));
}

[AvaloniaFact(Timeout = 15_000)]
public async Task ChatOutput_SameRunMixedContent_SplitsTextAndCallsWithoutLosingOrder()
{
var history = new ObservableCollection<AgentChatHistoryItem>
{
new()
{
Role = ChatRole.Assistant, AssistantRunId = "run",
Contents =
[
new TextContent("before segment"),
new FunctionCallContent("c1", "first_tool"),
new TextContent("between segments"),
new FunctionCallContent("c2", "second_tool"),
new TextContent("after segment"),
],
},
};
var sink = new RecordingSink();
using var model = new ChatOutputHtmlModel(history, new ObservableCollection<AgentChatRunningItem>(), () => true, sink);
await model.HistoryLoaded;
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Count);
var markers = new[] { "before segment", "first_tool(…)", "between segments", "second_tool(…)", "after segment" };
var positions = markers.Select(text => html.IndexOf(text, StringComparison.Ordinal)).ToArray();
Assert.All(positions, position => Assert.True(position >= 0));
Assert.True(positions.SequenceEqual(positions.OrderBy(position => position)));
Assert.DoesNotContain("between segments", html.Substring(
html.IndexOf("id=\"history-0-1-details\"", StringComparison.Ordinal),
html.IndexOf("</details>", html.IndexOf("id=\"history-0-1-details\"", StringComparison.Ordinal), StringComparison.Ordinal)
- html.IndexOf("id=\"history-0-1-details\"", StringComparison.Ordinal)));
}

[AvaloniaFact(Timeout = 15_000)]
public async Task ChatOutput_MixedCallNarrationResult_PreservesBindingOrderInLiveAndReload()
{
var item = new AgentChatHistoryItem
{
Role = ChatRole.Assistant, AssistantRunId = "run",
Contents =
[
new FunctionCallContent("c1", "first_tool"),
new TextContent("between call and result"),
new FunctionResultContent("c1", "result-after-text"),
],
};
var liveHistory = new ObservableCollection<AgentChatHistoryItem>();
var liveSink = new RecordingSink();
using var live = new ChatOutputHtmlModel(liveHistory, new ObservableCollection<AgentChatRunningItem>(),
() => true, liveSink);
await live.HistoryLoaded;
liveHistory.Add(item);
var reloadSink = new RecordingSink();
using var reload = new ChatOutputHtmlModel(new ObservableCollection<AgentChatHistoryItem> { item },
new ObservableCollection<AgentChatRunningItem>(), () => true, reloadSink);
await reload.HistoryLoaded;
foreach (var sink in new[] { liveSink, reloadSink })
{
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
var call = html.IndexOf("first_tool(…)", StringComparison.Ordinal);
var narration = html.IndexOf("between call and result", StringComparison.Ordinal);
var result = html.IndexOf("tool result: c1", StringComparison.Ordinal);
Assert.True(call >= 0 && call < narration && narration < result);
Assert.Contains("result-after-text", html);
}
}

[AvaloniaFact(Timeout = 15_000)]
public async Task ChatOutput_ResultAfterSeparateNarration_DoesNotInjectBeforeProse()
{
var items = new[]
{
ToolCallMessage("first_tool", "c1") with { AssistantRunId = "run" },
TextMessage(ChatRole.Assistant, "between call and result") with { AssistantRunId = "run" },
new AgentChatHistoryItem { Role = ChatRole.Tool, AssistantRunId = "run",
Contents = [new FunctionResultContent("c1", "after-narration")] },
};
var sink = new RecordingSink();
using var model = new ChatOutputHtmlModel(new ObservableCollection<AgentChatHistoryItem>(items),
new ObservableCollection<AgentChatRunningItem>(), () => true, sink);
await model.HistoryLoaded;
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
Assert.True(html.IndexOf("between call and result", StringComparison.Ordinal) <
html.IndexOf("tool result: c1", StringComparison.Ordinal));
Assert.Contains("after-narration", html);
}

[AvaloniaFact(Timeout = 15_000)]
public async Task ChatOutput_RunAcrossHistoryChunkBoundary_PreservesOneSummaryAndProseOrder()
{
var items = Enumerable.Range(0, 199).Select(i => TextMessage(ChatRole.User, $"user-{i}")).ToList();
items.Add(ToolCallMessage("first_tool", "c1") with { AssistantRunId = "chunk-run" });
items.Add(TextMessage(ChatRole.Assistant, "between calls") with { AssistantRunId = "chunk-run" });
items.Add(ToolCallMessage("second_tool", "c2") with { AssistantRunId = "chunk-run" });
var sink = new RecordingSink();
using var model = new ChatOutputHtmlModel(new ObservableCollection<AgentChatHistoryItem>(items),
new ObservableCollection<AgentChatRunningItem>(), () => true, sink);
await model.HistoryLoaded;
var html = string.Concat(sink.ContentOperations.Select(op => op.Content));
Assert.True(html.IndexOf("first_tool(…)", StringComparison.Ordinal) <
html.IndexOf("between calls", StringComparison.Ordinal));
Assert.True(html.IndexOf("between calls", StringComparison.Ordinal) <
html.IndexOf("second_tool(…)", StringComparison.Ordinal));
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(html, "data-assistant-run-id=\"chunk-run\"").Count);
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Count);
}

[AvaloniaFact(Timeout = 30_000)]
Expand Down Expand Up @@ -3011,8 +3114,7 @@ void CheckCompletion()
var reloadedHtml = string.Concat(reloadSink.ContentOperations.Select(op => op.Content));
foreach (var html in new[] { liveHtml, reloadedHtml })
{
Assert.Single(System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Cast<object>());
Assert.Contains("2 calls", html);
Assert.True(System.Text.RegularExpressions.Regex.Matches(html, "class=\"chat-content chat-tool-group\"").Count >= 1);
Assert.Contains("read-result", html);
Assert.Contains("write-result", html);
Assert.True(html.IndexOf("read(…)", StringComparison.Ordinal) <
Expand Down
Loading
Loading