Sync artifact sidebar with selected task
This commit is contained in:
parent
867629900a
commit
43db901e6f
@ -76,6 +76,8 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
bool _loadingSnapshot = false;
|
||||
bool _loadingPreview = false;
|
||||
bool _taskContextExpanded = false;
|
||||
int _snapshotLoadGeneration = 0;
|
||||
int _previewLoadGeneration = 0;
|
||||
Timer? _refreshTimer;
|
||||
|
||||
@override
|
||||
@ -88,14 +90,23 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
@override
|
||||
void didUpdateWidget(covariant AssistantArtifactSidebar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.sessionKey != widget.sessionKey ||
|
||||
final workspaceChanged =
|
||||
oldWidget.sessionKey != widget.sessionKey ||
|
||||
oldWidget.workspacePath != widget.workspacePath ||
|
||||
oldWidget.workspaceKind != widget.workspaceKind ||
|
||||
oldWidget.workspaceKind != widget.workspaceKind;
|
||||
if (workspaceChanged ||
|
||||
oldWidget.artifactSyncAtMs != widget.artifactSyncAtMs ||
|
||||
oldWidget.artifactSyncStatus != widget.artifactSyncStatus) {
|
||||
_activeTab = AssistantArtifactSidebarTab.files;
|
||||
_selectedEntry = null;
|
||||
_preview = const AssistantArtifactPreview.empty();
|
||||
if (workspaceChanged) {
|
||||
_activeTab = AssistantArtifactSidebarTab.files;
|
||||
_snapshot = null;
|
||||
_selectedEntry = null;
|
||||
_preview = const AssistantArtifactPreview.empty();
|
||||
_loadError = null;
|
||||
_loadingSnapshot = false;
|
||||
_loadingPreview = false;
|
||||
_previewLoadGeneration += 1;
|
||||
}
|
||||
unawaited(_refreshSnapshot());
|
||||
}
|
||||
_syncRefreshTimer();
|
||||
@ -461,13 +472,17 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
if (_loadingSnapshot) {
|
||||
return;
|
||||
}
|
||||
final generation = ++_snapshotLoadGeneration;
|
||||
final sessionKey = widget.sessionKey;
|
||||
setState(() {
|
||||
_loadingSnapshot = true;
|
||||
_loadError = null;
|
||||
});
|
||||
try {
|
||||
final snapshot = await widget.loadSnapshot();
|
||||
if (!mounted) {
|
||||
if (!mounted ||
|
||||
generation != _snapshotLoadGeneration ||
|
||||
sessionKey != widget.sessionKey) {
|
||||
return;
|
||||
}
|
||||
final nextSelected = _reconcileSelection(
|
||||
@ -484,7 +499,9 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
await _loadPreview(nextSelected);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
if (!mounted ||
|
||||
generation != _snapshotLoadGeneration ||
|
||||
sessionKey != widget.sessionKey) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
@ -555,13 +572,19 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
}
|
||||
|
||||
Future<void> _loadPreview(AssistantArtifactEntry entry) async {
|
||||
final generation = ++_previewLoadGeneration;
|
||||
final sessionKey = widget.sessionKey;
|
||||
final relativePath = entry.relativePath;
|
||||
setState(() {
|
||||
_loadingPreview = true;
|
||||
_preview = const AssistantArtifactPreview.empty();
|
||||
});
|
||||
try {
|
||||
final preview = await widget.loadPreview(entry);
|
||||
if (!mounted) {
|
||||
if (!mounted ||
|
||||
generation != _previewLoadGeneration ||
|
||||
sessionKey != widget.sessionKey ||
|
||||
relativePath != _selectedEntry?.relativePath) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
@ -569,7 +592,10 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
_loadingPreview = false;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
if (!mounted ||
|
||||
generation != _previewLoadGeneration ||
|
||||
sessionKey != widget.sessionKey ||
|
||||
relativePath != _selectedEntry?.relativePath) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/runtime/assistant_artifacts.dart';
|
||||
@ -46,6 +48,85 @@ void main() {
|
||||
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'clears stale artifacts and ignores late snapshot after task switch',
|
||||
(tester) async {
|
||||
final firstSnapshot = Completer<AssistantArtifactSnapshot>();
|
||||
var sessionKey = 'task-a';
|
||||
var workspacePath = '/tmp/task-a';
|
||||
|
||||
Future<AssistantArtifactSnapshot> loadSnapshot() {
|
||||
final capturedSessionKey = sessionKey;
|
||||
if (capturedSessionKey == 'task-a') {
|
||||
return firstSnapshot.future;
|
||||
}
|
||||
return Future<AssistantArtifactSnapshot>.value(
|
||||
AssistantArtifactSnapshot(
|
||||
workspacePath: '/tmp/task-b',
|
||||
workspaceKind: WorkspaceRefKind.localPath,
|
||||
fileEntries: const <AssistantArtifactEntry>[
|
||||
AssistantArtifactEntry(
|
||||
id: 'task-b-entry',
|
||||
label: 'task-b.md',
|
||||
relativePath: 'task-b.md',
|
||||
kind: AssistantArtifactEntryKind.file,
|
||||
mimeType: 'text/markdown',
|
||||
previewable: true,
|
||||
workspacePath: '/tmp/task-b',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
sessionKey: sessionKey,
|
||||
workspacePath: workspacePath,
|
||||
artifactSyncAtMs: 1,
|
||||
loadSnapshot: loadSnapshot,
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
sessionKey = 'task-b';
|
||||
workspacePath = '/tmp/task-b';
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
sessionKey: sessionKey,
|
||||
workspacePath: workspacePath,
|
||||
artifactSyncAtMs: 1,
|
||||
loadSnapshot: loadSnapshot,
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('task-a.md'), findsNothing);
|
||||
|
||||
firstSnapshot.complete(
|
||||
AssistantArtifactSnapshot(
|
||||
workspacePath: '/tmp/task-a',
|
||||
workspaceKind: WorkspaceRefKind.localPath,
|
||||
fileEntries: const <AssistantArtifactEntry>[
|
||||
AssistantArtifactEntry(
|
||||
id: 'task-a-entry',
|
||||
label: 'task-a.md',
|
||||
relativePath: 'task-a.md',
|
||||
kind: AssistantArtifactEntryKind.file,
|
||||
mimeType: 'text/markdown',
|
||||
previewable: true,
|
||||
workspacePath: '/tmp/task-a',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('task-a.md'), findsNothing);
|
||||
expect(find.text('task-b.md'), findsAtLeastNWidgets(1));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('keeps polling partial artifact snapshots', (tester) async {
|
||||
var loadCount = 0;
|
||||
|
||||
@ -242,6 +323,8 @@ void main() {
|
||||
}
|
||||
|
||||
Widget _buildTestApp({
|
||||
String sessionKey = 'unit-fixture-task-a',
|
||||
String workspacePath = '/tmp/thread',
|
||||
required double artifactSyncAtMs,
|
||||
String artifactSyncStatus = '',
|
||||
required Future<AssistantArtifactSnapshot> Function() loadSnapshot,
|
||||
@ -256,9 +339,9 @@ Widget _buildTestApp({
|
||||
width: 460,
|
||||
height: 640,
|
||||
child: AssistantArtifactSidebar(
|
||||
sessionKey: 'unit-fixture-task-a',
|
||||
sessionKey: sessionKey,
|
||||
threadTitle: 'Thread',
|
||||
workspacePath: '/tmp/thread',
|
||||
workspacePath: workspacePath,
|
||||
workspaceKind: WorkspaceRefKind.localPath,
|
||||
artifactSyncAtMs: artifactSyncAtMs,
|
||||
artifactSyncStatus: artifactSyncStatus,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user