Implement local-first single-agent artifact sync
This commit is contained in:
parent
ee452fc7ea
commit
0391039c18
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "vendor/codex"]
|
|
||||||
path = vendor/codex
|
|
||||||
url = https://github.com/openai/codex.git
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
// ignore_for_file: unused_import, unnecessary_import
|
// ignore_for_file: unused_import, unnecessary_import
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../i18n/app_language.dart';
|
import '../i18n/app_language.dart';
|
||||||
import '../models/app_models.dart';
|
import '../models/app_models.dart';
|
||||||
@ -164,6 +166,11 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
routing: routing,
|
routing: routing,
|
||||||
routingHint: 'single-agent',
|
routingHint: 'single-agent',
|
||||||
provider: effectiveProvider,
|
provider: effectiveProvider,
|
||||||
|
remoteWorkingDirectoryHint:
|
||||||
|
controller
|
||||||
|
.requireTaskThreadForSessionInternal(sessionKey)
|
||||||
|
.lastRemoteWorkingDirectory ??
|
||||||
|
'',
|
||||||
),
|
),
|
||||||
onUpdate: (update) {
|
onUpdate: (update) {
|
||||||
if (update.isDelta) {
|
if (update.isDelta) {
|
||||||
@ -175,7 +182,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
_applySingleAgentGoTaskResultDesktopInternal(
|
await _applySingleAgentGoTaskResultDesktopInternal(
|
||||||
controller,
|
controller,
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
sessionTarget: sessionTarget,
|
sessionTarget: sessionTarget,
|
||||||
@ -212,7 +219,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applySingleAgentGoTaskResultDesktopInternal(
|
Future<void> _applySingleAgentGoTaskResultDesktopInternal(
|
||||||
AppController controller, {
|
AppController controller, {
|
||||||
required String sessionKey,
|
required String sessionKey,
|
||||||
required AssistantExecutionTarget sessionTarget,
|
required AssistantExecutionTarget sessionTarget,
|
||||||
@ -220,7 +227,7 @@ void _applySingleAgentGoTaskResultDesktopInternal(
|
|||||||
required String thinking,
|
required String thinking,
|
||||||
required List<GatewayChatAttachmentPayload> attachments,
|
required List<GatewayChatAttachmentPayload> attachments,
|
||||||
required GoTaskServiceResult result,
|
required GoTaskServiceResult result,
|
||||||
}) {
|
}) async {
|
||||||
final resolvedRuntimeModel = result.resolvedModel.trim();
|
final resolvedRuntimeModel = result.resolvedModel.trim();
|
||||||
final resolvedGatewayEntryState = goTaskServiceGatewayEntryState(
|
final resolvedGatewayEntryState = goTaskServiceGatewayEntryState(
|
||||||
requestedTarget: sessionTarget,
|
requestedTarget: sessionTarget,
|
||||||
@ -233,13 +240,13 @@ void _applySingleAgentGoTaskResultDesktopInternal(
|
|||||||
lifecycleStatus: 'ready',
|
lifecycleStatus: 'ready',
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
lastResultCode: result.success ? 'success' : 'error',
|
lastResultCode: result.success ? 'success' : 'error',
|
||||||
|
lastRemoteWorkingDirectory: result.remoteWorkingDirectory.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: result.remoteWorkingDirectory.trim(),
|
||||||
|
lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind,
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
);
|
);
|
||||||
_updateSingleAgentWorkspaceBindingFromResultDesktopInternal(
|
await _persistSingleAgentArtifactsDesktopInternal(controller, sessionKey, result);
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
result,
|
|
||||||
);
|
|
||||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
controller.appendAssistantThreadMessageInternal(
|
controller.appendAssistantThreadMessageInternal(
|
||||||
@ -285,30 +292,107 @@ void _applySingleAgentGoTaskResultDesktopInternal(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateSingleAgentWorkspaceBindingFromResultDesktopInternal(
|
Future<void> _persistSingleAgentArtifactsDesktopInternal(
|
||||||
AppController controller,
|
AppController controller,
|
||||||
String sessionKey,
|
String sessionKey,
|
||||||
GoTaskServiceResult result,
|
GoTaskServiceResult result,
|
||||||
) {
|
) async {
|
||||||
final resolvedWorkspaceKind = result.resolvedWorkspaceRefKind;
|
final artifacts = result.artifacts;
|
||||||
final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim();
|
if (artifacts.isEmpty) {
|
||||||
if (resolvedWorkspaceKind == null || resolvedWorkingDirectory.isEmpty) {
|
controller.upsertTaskThreadInternal(
|
||||||
|
sessionKey,
|
||||||
|
lastArtifactSyncAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
lastArtifactSyncStatus: 'no-artifacts',
|
||||||
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final existingThread = controller.requireTaskThreadForSessionInternal(
|
final existingThread = controller.requireTaskThreadForSessionInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
);
|
);
|
||||||
|
if (existingThread.workspaceBinding.workspaceKind != WorkspaceKind.localFs) {
|
||||||
controller.upsertTaskThreadInternal(
|
controller.upsertTaskThreadInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
workspaceBinding: WorkspaceBinding(
|
lastArtifactSyncAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
workspaceId: existingThread.workspaceBinding.workspaceId,
|
lastArtifactSyncStatus: 'skipped-non-local-workspace',
|
||||||
workspaceKind: resolvedWorkspaceKind == WorkspaceRefKind.remotePath
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
? WorkspaceKind.remoteFs
|
);
|
||||||
: WorkspaceKind.localFs,
|
return;
|
||||||
workspacePath: resolvedWorkingDirectory,
|
}
|
||||||
displayPath: resolvedWorkingDirectory,
|
final root = Directory(existingThread.workspaceBinding.workspacePath);
|
||||||
writable: existingThread.workspaceBinding.writable,
|
await root.create(recursive: true);
|
||||||
),
|
|
||||||
|
var wroteArtifact = false;
|
||||||
|
for (final artifact in artifacts) {
|
||||||
|
if (!artifact.hasInlineContent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final relativePath = _sanitizeArtifactRelativePathInternal(
|
||||||
|
artifact.relativePath,
|
||||||
|
);
|
||||||
|
if (relativePath.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final target = await _nextArtifactTargetFileInternal(root, relativePath);
|
||||||
|
await target.parent.create(recursive: true);
|
||||||
|
await target.writeAsBytes(
|
||||||
|
_decodeArtifactContentInternal(artifact),
|
||||||
|
flush: true,
|
||||||
|
);
|
||||||
|
wroteArtifact = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.upsertTaskThreadInternal(
|
||||||
|
sessionKey,
|
||||||
|
lastArtifactSyncAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
lastArtifactSyncStatus: wroteArtifact ? 'synced' : 'no-inline-content',
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _sanitizeArtifactRelativePathInternal(String raw) {
|
||||||
|
final trimmed = raw.trim().replaceAll('\\', '/');
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
final cleaned = trimmed
|
||||||
|
.split('/')
|
||||||
|
.where((segment) => segment.isNotEmpty && segment != '.' && segment != '..')
|
||||||
|
.join('/');
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _decodeArtifactContentInternal(GoTaskServiceArtifact artifact) {
|
||||||
|
final encoding = artifact.encoding.trim().toLowerCase();
|
||||||
|
if (encoding == 'base64') {
|
||||||
|
return base64Decode(artifact.content);
|
||||||
|
}
|
||||||
|
return utf8.encode(artifact.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> _nextArtifactTargetFileInternal(
|
||||||
|
Directory root,
|
||||||
|
String relativePath,
|
||||||
|
) async {
|
||||||
|
final segments = relativePath.split('/');
|
||||||
|
final fileName = segments.removeLast();
|
||||||
|
final parent = segments.isEmpty
|
||||||
|
? root
|
||||||
|
: Directory('${root.path}/${segments.join('/')}');
|
||||||
|
final dotIndex = fileName.lastIndexOf('.');
|
||||||
|
final baseName = dotIndex <= 0 ? fileName : fileName.substring(0, dotIndex);
|
||||||
|
final extension = dotIndex <= 0 ? '' : fileName.substring(dotIndex);
|
||||||
|
var candidate = File('${parent.path}/$fileName');
|
||||||
|
if (!await candidate.exists()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
for (var version = 2; version < 1000; version += 1) {
|
||||||
|
candidate = File('${parent.path}/$baseName.v$version$extension');
|
||||||
|
if (!await candidate.exists()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return File(
|
||||||
|
'${parent.path}/$baseName.${DateTime.now().millisecondsSinceEpoch}$extension',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -276,6 +276,10 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
|||||||
String? lifecycleStatus,
|
String? lifecycleStatus,
|
||||||
double? lastRunAtMs,
|
double? lastRunAtMs,
|
||||||
String? lastResultCode,
|
String? lastResultCode,
|
||||||
|
String? lastRemoteWorkingDirectory,
|
||||||
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||||
|
double? lastArtifactSyncAtMs,
|
||||||
|
String? lastArtifactSyncStatus,
|
||||||
}) {
|
}) {
|
||||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@ -379,6 +383,10 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
|||||||
gatewayEntryState: gatewayEntryStateForTargetInternal(
|
gatewayEntryState: gatewayEntryStateForTargetInternal(
|
||||||
nextExecutionTarget,
|
nextExecutionTarget,
|
||||||
),
|
),
|
||||||
|
lastRemoteWorkingDirectory: null,
|
||||||
|
lastRemoteWorkspaceRefKind: null,
|
||||||
|
lastArtifactSyncAtMs: null,
|
||||||
|
lastArtifactSyncStatus: null,
|
||||||
))
|
))
|
||||||
.copyWith(
|
.copyWith(
|
||||||
messages: nextMessages,
|
messages: nextMessages,
|
||||||
@ -397,6 +405,10 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
|||||||
existing?.contextState.selectedSkillsSource,
|
existing?.contextState.selectedSkillsSource,
|
||||||
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
||||||
gatewayEntryState: gatewayEntryState,
|
gatewayEntryState: gatewayEntryState,
|
||||||
|
lastRemoteWorkingDirectory: lastRemoteWorkingDirectory,
|
||||||
|
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||||
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
||||||
|
lastArtifactSyncStatus: lastArtifactSyncStatus,
|
||||||
);
|
);
|
||||||
final nextStatus =
|
final nextStatus =
|
||||||
lifecycleStatus ??
|
lifecycleStatus ??
|
||||||
|
|||||||
@ -174,6 +174,7 @@ class GoTaskServiceRequest {
|
|||||||
this.routing,
|
this.routing,
|
||||||
this.routingHint = '',
|
this.routingHint = '',
|
||||||
this.provider = SingleAgentProvider.auto,
|
this.provider = SingleAgentProvider.auto,
|
||||||
|
this.remoteWorkingDirectoryHint = '',
|
||||||
this.resumeSession = false,
|
this.resumeSession = false,
|
||||||
this.collaborationMode = GoTaskServiceCollaborationMode.standard,
|
this.collaborationMode = GoTaskServiceCollaborationMode.standard,
|
||||||
this.multiAgent = false,
|
this.multiAgent = false,
|
||||||
@ -196,6 +197,7 @@ class GoTaskServiceRequest {
|
|||||||
final ExternalCodeAgentAcpRoutingConfig? routing;
|
final ExternalCodeAgentAcpRoutingConfig? routing;
|
||||||
final String routingHint;
|
final String routingHint;
|
||||||
final SingleAgentProvider provider;
|
final SingleAgentProvider provider;
|
||||||
|
final String remoteWorkingDirectoryHint;
|
||||||
final bool resumeSession;
|
final bool resumeSession;
|
||||||
final GoTaskServiceCollaborationMode collaborationMode;
|
final GoTaskServiceCollaborationMode collaborationMode;
|
||||||
final bool multiAgent;
|
final bool multiAgent;
|
||||||
@ -275,6 +277,8 @@ class GoTaskServiceRequest {
|
|||||||
)
|
)
|
||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
if (provider != SingleAgentProvider.auto) 'provider': provider.providerId,
|
if (provider != SingleAgentProvider.auto) 'provider': provider.providerId,
|
||||||
|
if (remoteWorkingDirectoryHint.trim().isNotEmpty)
|
||||||
|
'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(),
|
||||||
if (model.trim().isNotEmpty) 'model': model.trim(),
|
if (model.trim().isNotEmpty) 'model': model.trim(),
|
||||||
if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(),
|
if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(),
|
||||||
if (aiGatewayBaseUrl.trim().isNotEmpty)
|
if (aiGatewayBaseUrl.trim().isNotEmpty)
|
||||||
@ -370,6 +374,53 @@ class GoTaskServiceUpdate {
|
|||||||
bool get isDone => type == 'done' || payload['event'] == 'completed';
|
bool get isDone => type == 'done' || payload['event'] == 'completed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GoTaskServiceArtifact {
|
||||||
|
const GoTaskServiceArtifact({
|
||||||
|
required this.relativePath,
|
||||||
|
required this.label,
|
||||||
|
required this.contentType,
|
||||||
|
required this.encoding,
|
||||||
|
required this.content,
|
||||||
|
required this.downloadUrl,
|
||||||
|
required this.sizeBytes,
|
||||||
|
required this.sha256,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String relativePath;
|
||||||
|
final String label;
|
||||||
|
final String contentType;
|
||||||
|
final String encoding;
|
||||||
|
final String content;
|
||||||
|
final String downloadUrl;
|
||||||
|
final int? sizeBytes;
|
||||||
|
final String sha256;
|
||||||
|
|
||||||
|
bool get hasInlineContent => content.trim().isNotEmpty;
|
||||||
|
|
||||||
|
factory GoTaskServiceArtifact.fromJson(Map<String, dynamic> json) {
|
||||||
|
int? parseSize(Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value.toInt();
|
||||||
|
}
|
||||||
|
return int.tryParse(value?.toString() ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return GoTaskServiceArtifact(
|
||||||
|
relativePath: json['relativePath']?.toString().trim() ?? '',
|
||||||
|
label: json['label']?.toString().trim() ?? '',
|
||||||
|
contentType: json['contentType']?.toString().trim() ?? '',
|
||||||
|
encoding: json['encoding']?.toString().trim() ?? '',
|
||||||
|
content: json['content']?.toString() ?? '',
|
||||||
|
downloadUrl: json['downloadUrl']?.toString().trim() ?? '',
|
||||||
|
sizeBytes: parseSize(json['sizeBytes'] ?? json['size']),
|
||||||
|
sha256: json['sha256']?.toString().trim() ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GoTaskServiceResult {
|
class GoTaskServiceResult {
|
||||||
const GoTaskServiceResult({
|
const GoTaskServiceResult({
|
||||||
required this.success,
|
required this.success,
|
||||||
@ -395,6 +446,11 @@ class GoTaskServiceResult {
|
|||||||
raw['workingDirectory']?.toString().trim() ??
|
raw['workingDirectory']?.toString().trim() ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
String get resultSummary =>
|
||||||
|
raw['resultSummary']?.toString().trim().isNotEmpty == true
|
||||||
|
? raw['resultSummary'].toString().trim()
|
||||||
|
: raw['summary']?.toString().trim() ?? '';
|
||||||
|
|
||||||
String get resolvedExecutionTarget =>
|
String get resolvedExecutionTarget =>
|
||||||
raw['resolvedExecutionTarget']?.toString().trim() ?? '';
|
raw['resolvedExecutionTarget']?.toString().trim() ?? '';
|
||||||
|
|
||||||
@ -429,6 +485,22 @@ class GoTaskServiceResult {
|
|||||||
List<Map<String, dynamic>> get memorySources =>
|
List<Map<String, dynamic>> get memorySources =>
|
||||||
_castMapList(raw['memorySources']);
|
_castMapList(raw['memorySources']);
|
||||||
|
|
||||||
|
List<GoTaskServiceArtifact> get artifacts {
|
||||||
|
final rawArtifacts = raw['artifacts'];
|
||||||
|
if (rawArtifacts is! List) {
|
||||||
|
return const <GoTaskServiceArtifact>[];
|
||||||
|
}
|
||||||
|
return rawArtifacts
|
||||||
|
.whereType<Map>()
|
||||||
|
.map(
|
||||||
|
(item) => GoTaskServiceArtifact.fromJson(
|
||||||
|
item.cast<String, dynamic>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where((item) => item.relativePath.isNotEmpty)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
WorkspaceRefKind? get resolvedWorkspaceRefKind {
|
WorkspaceRefKind? get resolvedWorkspaceRefKind {
|
||||||
final rawValue = raw['resolvedWorkspaceRefKind']?.toString().trim() ?? '';
|
final rawValue = raw['resolvedWorkspaceRefKind']?.toString().trim() ?? '';
|
||||||
if (rawValue.isEmpty) {
|
if (rawValue.isEmpty) {
|
||||||
@ -436,6 +508,25 @@ class GoTaskServiceResult {
|
|||||||
}
|
}
|
||||||
return WorkspaceRefKindCopy.fromJsonValue(rawValue);
|
return WorkspaceRefKindCopy.fromJsonValue(rawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get remoteWorkingDirectory {
|
||||||
|
final remoteExecution = _castMap(raw['remoteExecution']);
|
||||||
|
return remoteExecution['remoteWorkingDirectory']?.toString().trim() ??
|
||||||
|
raw['remoteWorkingDirectory']?.toString().trim() ??
|
||||||
|
resolvedWorkingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRefKind? get remoteWorkspaceRefKind {
|
||||||
|
final remoteExecution = _castMap(raw['remoteExecution']);
|
||||||
|
final rawValue =
|
||||||
|
remoteExecution['remoteWorkspaceRefKind']?.toString().trim() ??
|
||||||
|
raw['remoteWorkspaceRefKind']?.toString().trim() ??
|
||||||
|
'';
|
||||||
|
if (rawValue.isEmpty) {
|
||||||
|
return resolvedWorkspaceRefKind;
|
||||||
|
}
|
||||||
|
return WorkspaceRefKindCopy.fromJsonValue(rawValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? goTaskServiceGatewayEntryState({
|
String? goTaskServiceGatewayEntryState({
|
||||||
|
|||||||
@ -765,6 +765,10 @@ class ThreadContextState {
|
|||||||
this.selectedModelSource = ThreadSelectionSource.inherited,
|
this.selectedModelSource = ThreadSelectionSource.inherited,
|
||||||
this.selectedSkillsSource = ThreadSelectionSource.inherited,
|
this.selectedSkillsSource = ThreadSelectionSource.inherited,
|
||||||
this.gatewayEntryState,
|
this.gatewayEntryState,
|
||||||
|
this.lastRemoteWorkingDirectory,
|
||||||
|
this.lastRemoteWorkspaceRefKind,
|
||||||
|
this.lastArtifactSyncAtMs,
|
||||||
|
this.lastArtifactSyncStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<GatewayChatMessage> messages;
|
final List<GatewayChatMessage> messages;
|
||||||
@ -777,6 +781,10 @@ class ThreadContextState {
|
|||||||
final ThreadSelectionSource selectedModelSource;
|
final ThreadSelectionSource selectedModelSource;
|
||||||
final ThreadSelectionSource selectedSkillsSource;
|
final ThreadSelectionSource selectedSkillsSource;
|
||||||
final String? gatewayEntryState;
|
final String? gatewayEntryState;
|
||||||
|
final String? lastRemoteWorkingDirectory;
|
||||||
|
final WorkspaceRefKind? lastRemoteWorkspaceRefKind;
|
||||||
|
final double? lastArtifactSyncAtMs;
|
||||||
|
final String? lastArtifactSyncStatus;
|
||||||
|
|
||||||
ThreadContextState copyWith({
|
ThreadContextState copyWith({
|
||||||
List<GatewayChatMessage>? messages,
|
List<GatewayChatMessage>? messages,
|
||||||
@ -790,6 +798,10 @@ class ThreadContextState {
|
|||||||
ThreadSelectionSource? selectedSkillsSource,
|
ThreadSelectionSource? selectedSkillsSource,
|
||||||
String? gatewayEntryState,
|
String? gatewayEntryState,
|
||||||
bool clearGatewayEntryState = false,
|
bool clearGatewayEntryState = false,
|
||||||
|
String? lastRemoteWorkingDirectory,
|
||||||
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||||
|
double? lastArtifactSyncAtMs,
|
||||||
|
String? lastArtifactSyncStatus,
|
||||||
}) {
|
}) {
|
||||||
return ThreadContextState(
|
return ThreadContextState(
|
||||||
messages: messages ?? this.messages,
|
messages: messages ?? this.messages,
|
||||||
@ -805,6 +817,13 @@ class ThreadContextState {
|
|||||||
gatewayEntryState: clearGatewayEntryState
|
gatewayEntryState: clearGatewayEntryState
|
||||||
? null
|
? null
|
||||||
: (gatewayEntryState ?? this.gatewayEntryState),
|
: (gatewayEntryState ?? this.gatewayEntryState),
|
||||||
|
lastRemoteWorkingDirectory:
|
||||||
|
lastRemoteWorkingDirectory ?? this.lastRemoteWorkingDirectory,
|
||||||
|
lastRemoteWorkspaceRefKind:
|
||||||
|
lastRemoteWorkspaceRefKind ?? this.lastRemoteWorkspaceRefKind,
|
||||||
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs ?? this.lastArtifactSyncAtMs,
|
||||||
|
lastArtifactSyncStatus:
|
||||||
|
lastArtifactSyncStatus ?? this.lastArtifactSyncStatus,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -822,10 +841,21 @@ class ThreadContextState {
|
|||||||
'selectedModelSource': selectedModelSource.name,
|
'selectedModelSource': selectedModelSource.name,
|
||||||
'selectedSkillsSource': selectedSkillsSource.name,
|
'selectedSkillsSource': selectedSkillsSource.name,
|
||||||
'gatewayEntryState': gatewayEntryState,
|
'gatewayEntryState': gatewayEntryState,
|
||||||
|
'lastRemoteWorkingDirectory': lastRemoteWorkingDirectory,
|
||||||
|
'lastRemoteWorkspaceRefKind': lastRemoteWorkspaceRefKind?.name,
|
||||||
|
'lastArtifactSyncAtMs': lastArtifactSyncAtMs,
|
||||||
|
'lastArtifactSyncStatus': lastArtifactSyncStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ThreadContextState.fromJson(Map<String, dynamic> json) {
|
factory ThreadContextState.fromJson(Map<String, dynamic> json) {
|
||||||
|
double? asDouble(Object? value) {
|
||||||
|
if (value is num) {
|
||||||
|
return value.toDouble();
|
||||||
|
}
|
||||||
|
return double.tryParse(value?.toString() ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
final rawMessages = json['messages'];
|
final rawMessages = json['messages'];
|
||||||
final messages = rawMessages is List
|
final messages = rawMessages is List
|
||||||
? rawMessages
|
? rawMessages
|
||||||
@ -876,6 +906,18 @@ class ThreadContextState {
|
|||||||
json['selectedSkillsSource']?.toString(),
|
json['selectedSkillsSource']?.toString(),
|
||||||
),
|
),
|
||||||
gatewayEntryState: json['gatewayEntryState']?.toString(),
|
gatewayEntryState: json['gatewayEntryState']?.toString(),
|
||||||
|
lastRemoteWorkingDirectory:
|
||||||
|
json['lastRemoteWorkingDirectory']?.toString(),
|
||||||
|
lastRemoteWorkspaceRefKind: (() {
|
||||||
|
final rawValue =
|
||||||
|
json['lastRemoteWorkspaceRefKind']?.toString().trim() ?? '';
|
||||||
|
if (rawValue.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return WorkspaceRefKindCopy.fromJsonValue(rawValue);
|
||||||
|
})(),
|
||||||
|
lastArtifactSyncAtMs: asDouble(json['lastArtifactSyncAtMs']),
|
||||||
|
lastArtifactSyncStatus: json['lastArtifactSyncStatus']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -958,6 +1000,10 @@ class TaskThread {
|
|||||||
String? latestResolvedRuntimeModel,
|
String? latestResolvedRuntimeModel,
|
||||||
double? lastRunAtMs,
|
double? lastRunAtMs,
|
||||||
String? lastResultCode,
|
String? lastResultCode,
|
||||||
|
String? lastRemoteWorkingDirectory,
|
||||||
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||||
|
double? lastArtifactSyncAtMs,
|
||||||
|
String? lastArtifactSyncStatus,
|
||||||
}) : threadId = _resolveThreadId(threadId),
|
}) : threadId = _resolveThreadId(threadId),
|
||||||
title = title ?? '',
|
title = title ?? '',
|
||||||
ownerScope =
|
ownerScope =
|
||||||
@ -992,6 +1038,16 @@ class TaskThread {
|
|||||||
latestResolvedRuntimeModel:
|
latestResolvedRuntimeModel:
|
||||||
latestResolvedRuntimeModel?.trim() ?? '',
|
latestResolvedRuntimeModel?.trim() ?? '',
|
||||||
gatewayEntryState: gatewayEntryState?.trim(),
|
gatewayEntryState: gatewayEntryState?.trim(),
|
||||||
|
lastRemoteWorkingDirectory:
|
||||||
|
lastRemoteWorkingDirectory?.trim().isNotEmpty == true
|
||||||
|
? lastRemoteWorkingDirectory!.trim()
|
||||||
|
: null,
|
||||||
|
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||||
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
||||||
|
lastArtifactSyncStatus:
|
||||||
|
lastArtifactSyncStatus?.trim().isNotEmpty == true
|
||||||
|
? lastArtifactSyncStatus!.trim()
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
lifecycleState =
|
lifecycleState =
|
||||||
lifecycleState ??
|
lifecycleState ??
|
||||||
@ -1024,6 +1080,12 @@ class TaskThread {
|
|||||||
String get assistantModelId => contextState.selectedModelId;
|
String get assistantModelId => contextState.selectedModelId;
|
||||||
AssistantMessageViewMode get messageViewMode => contextState.messageViewMode;
|
AssistantMessageViewMode get messageViewMode => contextState.messageViewMode;
|
||||||
String? get gatewayEntryState => contextState.gatewayEntryState;
|
String? get gatewayEntryState => contextState.gatewayEntryState;
|
||||||
|
String? get lastRemoteWorkingDirectory =>
|
||||||
|
contextState.lastRemoteWorkingDirectory;
|
||||||
|
WorkspaceRefKind? get lastRemoteWorkspaceRefKind =>
|
||||||
|
contextState.lastRemoteWorkspaceRefKind;
|
||||||
|
double? get lastArtifactSyncAtMs => contextState.lastArtifactSyncAtMs;
|
||||||
|
String? get lastArtifactSyncStatus => contextState.lastArtifactSyncStatus;
|
||||||
String get latestResolvedRuntimeModel =>
|
String get latestResolvedRuntimeModel =>
|
||||||
contextState.latestResolvedRuntimeModel;
|
contextState.latestResolvedRuntimeModel;
|
||||||
bool get hasExplicitExecutionTargetSelection =>
|
bool get hasExplicitExecutionTargetSelection =>
|
||||||
@ -1060,6 +1122,10 @@ class TaskThread {
|
|||||||
String? gatewayEntryState,
|
String? gatewayEntryState,
|
||||||
bool clearGatewayEntryState = false,
|
bool clearGatewayEntryState = false,
|
||||||
String? latestResolvedRuntimeModel,
|
String? latestResolvedRuntimeModel,
|
||||||
|
String? lastRemoteWorkingDirectory,
|
||||||
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||||
|
double? lastArtifactSyncAtMs,
|
||||||
|
String? lastArtifactSyncStatus,
|
||||||
}) {
|
}) {
|
||||||
return TaskThread(
|
return TaskThread(
|
||||||
threadId: threadId ?? this.threadId,
|
threadId: threadId ?? this.threadId,
|
||||||
@ -1078,6 +1144,10 @@ class TaskThread {
|
|||||||
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
||||||
gatewayEntryState: gatewayEntryState,
|
gatewayEntryState: gatewayEntryState,
|
||||||
clearGatewayEntryState: clearGatewayEntryState,
|
clearGatewayEntryState: clearGatewayEntryState,
|
||||||
|
lastRemoteWorkingDirectory: lastRemoteWorkingDirectory,
|
||||||
|
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||||
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
||||||
|
lastArtifactSyncStatus: lastArtifactSyncStatus,
|
||||||
),
|
),
|
||||||
lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith(
|
lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith(
|
||||||
archived: archived,
|
archived: archived,
|
||||||
@ -1213,6 +1283,10 @@ class TaskThread {
|
|||||||
'selectedModelSource': json['assistantModelSource'],
|
'selectedModelSource': json['assistantModelSource'],
|
||||||
'selectedSkillsSource': json['selectedSkillsSource'],
|
'selectedSkillsSource': json['selectedSkillsSource'],
|
||||||
'gatewayEntryState': json['gatewayEntryState'],
|
'gatewayEntryState': json['gatewayEntryState'],
|
||||||
|
'lastRemoteWorkingDirectory': json['lastRemoteWorkingDirectory'],
|
||||||
|
'lastRemoteWorkspaceRefKind': json['lastRemoteWorkspaceRefKind'],
|
||||||
|
'lastArtifactSyncAtMs': json['lastArtifactSyncAtMs'],
|
||||||
|
'lastArtifactSyncStatus': json['lastArtifactSyncStatus'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -96,7 +96,7 @@ List<ManagedMountTargetState> withAvailableMountTargetsInternal(
|
|||||||
|
|
||||||
Future<void> waitForInternal(
|
Future<void> waitForInternal(
|
||||||
bool Function() predicate, {
|
bool Function() predicate, {
|
||||||
Duration timeout = const Duration(seconds: 5),
|
Duration timeout = const Duration(seconds: 20),
|
||||||
}) async {
|
}) async {
|
||||||
final deadline = DateTime.now().add(timeout);
|
final deadline = DateTime.now().add(timeout);
|
||||||
while (!predicate()) {
|
while (!predicate()) {
|
||||||
|
|||||||
@ -858,7 +858,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController rebinds local Single Agent threads to the structured resolved directory',
|
'AppController keeps local Single Agent threads bound to the local workspace while recording remote metadata',
|
||||||
() async {
|
() async {
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
'xworkmate-single-agent-remote-thread-cwd-',
|
'xworkmate-single-agent-remote-thread-cwd-',
|
||||||
@ -918,29 +918,39 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
await controller.switchSession('draft:remote-thread');
|
await controller.switchSession('draft:remote-thread');
|
||||||
|
|
||||||
await controller.sendChatMessage('第一次运行', thinking: 'low');
|
await controller.sendChatMessage('第一次运行', thinking: 'low');
|
||||||
|
final localThreadDir =
|
||||||
|
'${defaultWorkspace.path}/.xworkmate/threads/draft-remote-thread';
|
||||||
|
const remoteThreadDir =
|
||||||
|
'/opt/data/.xworkmate/threads/draft-remote-thread';
|
||||||
expect(
|
expect(
|
||||||
client.requests.first.workingDirectory,
|
client.requests.first.workingDirectory,
|
||||||
'${defaultWorkspace.path}/.xworkmate/threads/draft-remote-thread',
|
localThreadDir,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller.assistantWorkspacePathForSession('draft:remote-thread'),
|
controller.assistantWorkspacePathForSession('draft:remote-thread'),
|
||||||
'/opt/data/.xworkmate/threads/draft-remote-thread',
|
localThreadDir,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller.assistantWorkspaceKindForSession('draft:remote-thread'),
|
controller.assistantWorkspaceKindForSession('draft:remote-thread'),
|
||||||
WorkspaceRefKind.localPath,
|
WorkspaceRefKind.localPath,
|
||||||
);
|
);
|
||||||
|
final thread = controller.requireTaskThreadForSessionInternal(
|
||||||
|
'draft:remote-thread',
|
||||||
|
);
|
||||||
|
expect(thread.lastRemoteWorkingDirectory, remoteThreadDir);
|
||||||
|
expect(thread.lastRemoteWorkspaceRefKind, WorkspaceRefKind.localPath);
|
||||||
|
|
||||||
await controller.sendChatMessage('第二次运行', thinking: 'low');
|
await controller.sendChatMessage('第二次运行', thinking: 'low');
|
||||||
expect(
|
expect(
|
||||||
client.requests.last.workingDirectory,
|
client.requests.last.workingDirectory,
|
||||||
'/opt/data/.xworkmate/threads/draft-remote-thread',
|
localThreadDir,
|
||||||
);
|
);
|
||||||
|
expect(client.requests.last.remoteWorkingDirectoryHint, remoteThreadDir);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController rebinds remote Single Agent threads to the resolved thread directory',
|
'AppController keeps remote Single Agent threads on the local workspace and forwards the remote directory as a hint',
|
||||||
() async {
|
() async {
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
'xworkmate-single-agent-remote-rebind-cwd-',
|
'xworkmate-single-agent-remote-rebind-cwd-',
|
||||||
@ -1013,27 +1023,145 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
await controller.switchSession('draft:remote-thread');
|
await controller.switchSession('draft:remote-thread');
|
||||||
|
|
||||||
await controller.sendChatMessage('第一次运行', thinking: 'low');
|
await controller.sendChatMessage('第一次运行', thinking: 'low');
|
||||||
|
final localThreadDir =
|
||||||
|
'${defaultWorkspace.path}/.xworkmate/threads/draft-remote-thread';
|
||||||
expect(
|
expect(
|
||||||
client.requests.first.workingDirectory,
|
client.requests.first.workingDirectory,
|
||||||
'${defaultWorkspace.path}/.xworkmate/threads/draft-remote-thread',
|
localThreadDir,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller.assistantWorkspacePathForSession('draft:remote-thread'),
|
controller.assistantWorkspacePathForSession('draft:remote-thread'),
|
||||||
'/remote/threads/task-42',
|
localThreadDir,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller.assistantWorkspaceKindForSession('draft:remote-thread'),
|
controller.assistantWorkspaceKindForSession('draft:remote-thread'),
|
||||||
WorkspaceRefKind.remotePath,
|
WorkspaceRefKind.localPath,
|
||||||
);
|
);
|
||||||
|
final thread = controller.requireTaskThreadForSessionInternal(
|
||||||
|
'draft:remote-thread',
|
||||||
|
);
|
||||||
|
expect(thread.lastRemoteWorkingDirectory, '/remote/threads/task-42');
|
||||||
|
expect(thread.lastRemoteWorkspaceRefKind, WorkspaceRefKind.remotePath);
|
||||||
|
|
||||||
await controller.sendChatMessage('第二次运行', thinking: 'low');
|
await controller.sendChatMessage('第二次运行', thinking: 'low');
|
||||||
expect(
|
expect(
|
||||||
client.requests.last.workingDirectory,
|
client.requests.last.workingDirectory,
|
||||||
|
localThreadDir,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
client.requests.last.remoteWorkingDirectoryHint,
|
||||||
'/remote/threads/task-42',
|
'/remote/threads/task-42',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'AppController writes returned Single Agent artifacts into the local workspace and versions name conflicts',
|
||||||
|
() async {
|
||||||
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
|
'xworkmate-single-agent-artifact-sync-',
|
||||||
|
);
|
||||||
|
final defaultWorkspace = Directory(
|
||||||
|
'${tempDirectory.path}/default-workspace',
|
||||||
|
);
|
||||||
|
await defaultWorkspace.create(recursive: true);
|
||||||
|
|
||||||
|
final store = createStoreFromTempDirectoryInternal(tempDirectory);
|
||||||
|
await store.initialize();
|
||||||
|
await store.saveSettingsSnapshot(
|
||||||
|
SettingsSnapshot.defaults().copyWith(
|
||||||
|
workspacePath: defaultWorkspace.path,
|
||||||
|
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final artifactPayload = base64Encode(utf8.encode('new report body'));
|
||||||
|
final client = FakeGoTaskServiceClientInternal(
|
||||||
|
capabilities: ExternalCodeAgentAcpCapabilities(
|
||||||
|
singleAgent: true,
|
||||||
|
multiAgent: false,
|
||||||
|
providers: <SingleAgentProvider>{SingleAgentProvider.opencode},
|
||||||
|
raw: <String, dynamic>{},
|
||||||
|
),
|
||||||
|
result: GoTaskServiceResult(
|
||||||
|
success: true,
|
||||||
|
message: 'ARTIFACT_OK',
|
||||||
|
turnId: 'turn-artifact-1',
|
||||||
|
raw: <String, dynamic>{
|
||||||
|
'resolvedWorkingDirectory': '/remote/threads/artifact-thread',
|
||||||
|
'resolvedWorkspaceRefKind': 'remotePath',
|
||||||
|
'artifacts': <Map<String, dynamic>>[
|
||||||
|
<String, dynamic>{
|
||||||
|
'relativePath': 'outputs/report.docx',
|
||||||
|
'label': 'Report',
|
||||||
|
'contentType':
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'encoding': 'base64',
|
||||||
|
'content': artifactPayload,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
errorMessage: '',
|
||||||
|
resolvedModel: '',
|
||||||
|
route: GoTaskServiceRoute.externalAcpSingle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final controller = await createAppControllerInternal(
|
||||||
|
store: store,
|
||||||
|
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||||
|
SingleAgentProvider.opencode,
|
||||||
|
],
|
||||||
|
runtimeCoordinator: RuntimeCoordinator(
|
||||||
|
gateway: FakeGatewayRuntimeInternal(store: store),
|
||||||
|
codex: FakeCodexRuntimeInternal(),
|
||||||
|
),
|
||||||
|
goTaskServiceClient: client,
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.initializeAssistantThreadContext(
|
||||||
|
'draft:artifact-thread',
|
||||||
|
title: 'Artifact Thread',
|
||||||
|
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||||
|
);
|
||||||
|
await controller.switchSession('draft:artifact-thread');
|
||||||
|
|
||||||
|
final localThreadDir = Directory(
|
||||||
|
'${defaultWorkspace.path}/.xworkmate/threads/draft-artifact-thread',
|
||||||
|
);
|
||||||
|
await localThreadDir.create(recursive: true);
|
||||||
|
final existingArtifact = File('${localThreadDir.path}/outputs/report.docx');
|
||||||
|
await existingArtifact.parent.create(recursive: true);
|
||||||
|
await existingArtifact.writeAsString('old report body');
|
||||||
|
|
||||||
|
await controller.sendChatMessage('生成文档', thinking: 'low');
|
||||||
|
|
||||||
|
final versionedArtifact = File(
|
||||||
|
'${localThreadDir.path}/outputs/report.v2.docx',
|
||||||
|
);
|
||||||
|
expect(existingArtifact.existsSync(), isTrue);
|
||||||
|
expect(versionedArtifact.existsSync(), isTrue);
|
||||||
|
expect(await versionedArtifact.readAsString(), 'new report body');
|
||||||
|
expect(
|
||||||
|
controller.assistantWorkspacePathForSession('draft:artifact-thread'),
|
||||||
|
localThreadDir.path,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
controller.assistantWorkspaceKindForSession('draft:artifact-thread'),
|
||||||
|
WorkspaceRefKind.localPath,
|
||||||
|
);
|
||||||
|
final thread = controller.requireTaskThreadForSessionInternal(
|
||||||
|
'draft:artifact-thread',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
thread.lastRemoteWorkingDirectory,
|
||||||
|
'/remote/threads/artifact-thread',
|
||||||
|
);
|
||||||
|
expect(thread.lastRemoteWorkspaceRefKind, WorkspaceRefKind.remotePath);
|
||||||
|
expect(thread.lastArtifactSyncStatus, 'synced');
|
||||||
|
expect(thread.lastArtifactSyncAtMs, isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController keeps local Codex-style working directories for remote thread refs',
|
'AppController keeps local Codex-style working directories for remote thread refs',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
1
vendor/codex
vendored
1
vendor/codex
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit 78280f872a58dfbb51d2883791d036db00cbfe0f
|
|
||||||
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB |
@ -1,49 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!--
|
|
||||||
If you are serving your web app in a path other than the root, change the
|
|
||||||
href value below to reflect the base path you are serving from.
|
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
|
||||||
it to work correctly.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="XWorkmate Web keeps the Assistant-first workflow with Single Agent and Relay OpenClaw Gateway."
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="XWorkmate">
|
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
|
||||||
|
|
||||||
<title>XWorkmate</title>
|
|
||||||
<link rel="manifest" href="manifest.json">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--
|
|
||||||
You can customize the "flutter_bootstrap.js" script.
|
|
||||||
This is useful to provide a custom configuration to the Flutter loader
|
|
||||||
or to give the user feedback during the initialization process.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://docs.flutter.dev/platform-integration/web/initialization
|
|
||||||
-->
|
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "XWorkmate",
|
|
||||||
"short_name": "XWorkmate",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#0175C2",
|
|
||||||
"theme_color": "#0175C2",
|
|
||||||
"description": "Assistant-first Flutter Web shell for Single Agent and Relay OpenClaw Gateway.",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"prefer_related_applications": false,
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user