Merge branch 'codex/strict-taskthread-workspacebinding'
This commit is contained in:
commit
b4b6e642cc
@ -9,10 +9,10 @@
|
||||
1. `TaskThread` 是任务线程的唯一主对象。
|
||||
2. UI 保持现有结构不变,但线程选择的唯一键是 `TaskThread.threadId`。
|
||||
3. UI 选中线程后,系统必须读取完整 `TaskThread`,而不是从页面状态拼装线程信息。
|
||||
4. `TaskThread` 持久化 schema 保持不变;本轮只迁移 runtime 解释层与执行链路。
|
||||
4. `TaskThread` 持久化 schema 保持不变,但 `workspaceBinding` 在 create/load 时必须完整;缺失 binding 的旧记录按非法数据处理并跳过加载。
|
||||
5. 执行请求由 controller / runtime 根据 `ownerScope / workspaceBinding / executionBinding / contextState` 构造。
|
||||
6. controller / runtime 统一通过 `Go Agent-core` 调度执行:Desktop 走 App 内 local bridge,Web 走远端 ACP / RPC endpoint。
|
||||
7. 执行结果先回写 `TaskThread.contextState`,主体区域同步显示;只有必要时才更新 `workspaceBinding`;右栏展示读取的是当前 `TaskThread` 最新记录。
|
||||
7. 执行结果先回写 `TaskThread.contextState`,主体区域同步显示;UI 与执行始终只读取当前 `TaskThread.workspaceBinding`,不再存在 runtime first-binding 或 fallback 到 `main`。
|
||||
|
||||
## 2. TaskThread 结构
|
||||
|
||||
@ -112,7 +112,7 @@ ThreadLifecycleState
|
||||
职责:
|
||||
|
||||
- `archived`:归档标记
|
||||
- `status`:线程生命周期摘要,例如 `needs_workspace / ready`
|
||||
- `status`:线程生命周期摘要,例如 `ready / error`
|
||||
- `lastRunAtMs / lastResultCode`:最近执行摘要
|
||||
|
||||
## 3. TaskThread 生命周期主链
|
||||
@ -136,7 +136,7 @@ flowchart LR
|
||||
F --> G["执行结果"]
|
||||
|
||||
G --> H["回写线程上下文\n(主体区域 同步显示)"]
|
||||
G --> I["必要时更新 workspaceBinding"]
|
||||
G --> I["仅显式更新当前线程 workspaceBinding"]
|
||||
|
||||
H --> J["右栏显示"]
|
||||
I --> J
|
||||
@ -149,8 +149,8 @@ flowchart LR
|
||||
3. `构造执行请求` 属于 agent-core / runtime 协调层,不属于 UI。
|
||||
4. `Go Agent-core` 是唯一执行调度面;Desktop / Web 共用同一套 session 语义,只在 transport 上有差异。
|
||||
5. `回写线程上下文` 是执行结束后的第一落点;主体区域同步显示依赖这一回写。
|
||||
6. `必要时更新 workspaceBinding` 是条件分支,不是固定每次都发生的回写步骤。
|
||||
7. `右栏显示` 读取的是当前 `TaskThread` 最新记录,因此它与主体区域共享同一线程事实来源。
|
||||
6. `workspaceBinding` 不是运行时补齐对象;线程在 create/load 时必须已经完整。
|
||||
7. `右栏显示` 与执行请求都读取当前 `TaskThread.workspaceBinding`,因此它与主体区域共享同一线程事实来源。
|
||||
|
||||
## 4. 当前设计约束
|
||||
|
||||
@ -170,7 +170,7 @@ flowchart LR
|
||||
### 4.3 TaskThread 约束
|
||||
|
||||
- `threadId` 是线程身份唯一键。
|
||||
- `workspaceBinding` 是线程生命周期字段,不再承担 fallback 猜测语义。
|
||||
- `workspaceBinding` 是 `TaskThread` 的必填生命周期字段;create/load 阶段缺失即为非法线程数据。
|
||||
- `contextState` 是线程上下文真相源。
|
||||
- `lifecycleState` 只表达归档与生命周期摘要,不替代线程主体模型。
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ currentSessionKey
|
||||
这条链路说明:
|
||||
|
||||
1. 真正决定执行目录的是 `sessionKey`
|
||||
2. `workspace_root` 文本上下文本身不会自动进入线程绑定
|
||||
2. `workspace_root` 文本上下文本身不会创建 first binding
|
||||
3. 空 `sessionKey` 会被归一为 `main`
|
||||
4. 一旦多个任务线没有真正切换到独立 `sessionKey`,它们就会共享 `main`
|
||||
|
||||
@ -51,7 +51,7 @@ currentSessionKey
|
||||
3. `TaskThread.threadId` 就是该任务线的 `sessionKey`。
|
||||
4. 任何可触发执行的入口都不得在空 `sessionKey` 下运行。
|
||||
5. 对非主线程任务线,禁止 silent fallback 到 `main`。
|
||||
6. `workspace_root` 不是线程身份;它最多只能作为该线程 `workspaceBinding` 的输入之一。
|
||||
6. `workspace_root` 不是线程身份;它只能更新当前已存在线程的 `workspaceBinding`,不能创建 first binding。
|
||||
|
||||
换句话说:
|
||||
|
||||
@ -146,7 +146,7 @@ request.workingDirectory == current TaskThread.workspaceBinding.workspacePath
|
||||
|
||||
- 空 `sessionKey -> main` 只允许用于真正的主线程初始化阶段
|
||||
- 非主线程任务线若缺少 `sessionKey`,状态应为 `needs_binding` 或 `not_runnable`
|
||||
- 非主线程任务线若缺少工作路径,状态应为 `needs_workspace`
|
||||
- 本地可执行线程在 create/load 阶段必须已经拥有唯一工作目录
|
||||
- 不允许继续执行并偷偷落到 `threads/main`
|
||||
|
||||
## 5. `workspace_root` 的正确角色
|
||||
@ -161,15 +161,14 @@ request.workingDirectory == current TaskThread.workspaceBinding.workspacePath
|
||||
|
||||
它可以是:
|
||||
|
||||
- 创建新线程时的初始工作区候选根目录
|
||||
- 外部 provider / 执行上下文导入时的显式 workspace bootstrap 输入
|
||||
- 当前线程 `workspaceBinding` 的一次用户确认更新来源
|
||||
- 当前线程 `workspaceBinding` 的一次显式更新来源
|
||||
- 外部 provider / 执行上下文导入时对当前线程 binding 的确认更新输入
|
||||
|
||||
因此正确顺序应为:
|
||||
|
||||
```text
|
||||
Execution context.workspace_root
|
||||
-> bind/update current TaskThread.workspaceBinding
|
||||
-> update current TaskThread.workspaceBinding
|
||||
-> persist on that TaskThread
|
||||
-> subsequent execute reads TaskThread.workspaceBinding.workspacePath
|
||||
```
|
||||
@ -280,7 +279,7 @@ single-agent 入口必须在执行前验证:
|
||||
|
||||
1. 历史共享 `main` 的记录继续作为 `main`
|
||||
2. 从修正版本开始,新建任务线必须创建独立 `sessionKey`
|
||||
3. 对已暴露出共享问题的入口,优先阻止继续 silent fallback
|
||||
3. 对已暴露出共享问题的入口,优先阻止继续 silent fallback,并移除 runtime first-binding
|
||||
|
||||
### 8.3 未绑定任务线
|
||||
|
||||
@ -303,7 +302,7 @@ single-agent 入口必须在执行前验证:
|
||||
3. 切换任务线后,`currentSessionKey` 与右栏路径同步变化。
|
||||
4. single-agent 请求里的 `sessionId / threadId / workingDirectory` 始终对应当前线程。
|
||||
5. 任意非主线程缺少 `sessionKey` 时,执行被阻止,而不是回落到 `main`。
|
||||
6. `workspace_root` 被当作线程 binding 输入处理,而不是 prompt-only 文本或跨线程覆盖指令。
|
||||
6. `workspace_root` 被当作当前线程 binding 的显式更新输入处理,而不是 prompt-only 文本、first-binding 指令或跨线程覆盖指令。
|
||||
|
||||
## 10. 与现有架构文档的关系
|
||||
|
||||
|
||||
@ -181,6 +181,8 @@ Ownership summary:
|
||||
|
||||
- 如果值已经存在于 `TaskThread`,则线程值优先于 Settings 默认值
|
||||
- `TaskThread` 是当前线程展示与执行的唯一主对象
|
||||
- `TaskThread` 在 create/load 时必须已经拥有完整 `workspaceBinding`
|
||||
- 缺少 `workspaceBinding` 的旧记录属于非法线程数据,应在恢复阶段跳过并通过启动告警暴露
|
||||
|
||||
### 3.3 Agent-Core / Runtime 协调状态
|
||||
|
||||
@ -196,6 +198,7 @@ Primary responsibilities:
|
||||
- 请求构造不属于 UI
|
||||
- Flutter UI 不直接承担 runtime dispatch 职责
|
||||
- 工作空间选择不再通过旧式运行前猜测获得
|
||||
- 不允许 runtime fallback 到 `main`、`Directory.current` 或 prompt first-binding
|
||||
- 结果回写先更新线程上下文,再驱动主体区域与右栏刷新
|
||||
- Desktop / Web 共用相同 session 生命周期;不再单独发明 relay-only 执行协议
|
||||
|
||||
|
||||
@ -357,6 +357,7 @@ class AppController extends ChangeNotifier {
|
||||
String settingsDraftStatusMessageInternal = '';
|
||||
bool initializingInternal = true;
|
||||
String? bootstrapErrorInternal;
|
||||
String? startupTaskThreadWarningInternal;
|
||||
StreamSubscription<GatewayPushEvent>? runtimeEventsSubscriptionInternal;
|
||||
bool disposedInternal = false;
|
||||
String resolvedUserHomeDirectoryInternal = resolveUserHomeDirectory();
|
||||
@ -414,6 +415,15 @@ class AppController extends ChangeNotifier {
|
||||
DetailPanelData? get detailPanel => detailPanelInternal;
|
||||
bool get initializing => initializingInternal;
|
||||
String? get bootstrapError => bootstrapErrorInternal;
|
||||
String? get startupTaskThreadWarning => startupTaskThreadWarningInternal;
|
||||
|
||||
void dismissStartupTaskThreadWarning() {
|
||||
if ((startupTaskThreadWarningInternal ?? '').trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
startupTaskThreadWarningInternal = null;
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
UiFeatureAccess featuresFor(UiFeaturePlatform platform) {
|
||||
final manifest = applyAppleAppStorePolicy(
|
||||
|
||||
@ -449,6 +449,13 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
await skillDirectoryAccessServiceInternal.resolveUserHomeDirectory();
|
||||
await settingsControllerInternal.initialize();
|
||||
final storedAssistantThreads = await storeInternal.loadTaskThreads();
|
||||
final skippedInvalidThreadIds = storeInternal.lastSkippedInvalidTaskThreadIds;
|
||||
startupTaskThreadWarningInternal = skippedInvalidThreadIds.isEmpty
|
||||
? null
|
||||
: appText(
|
||||
'已跳过 ${skippedInvalidThreadIds.length} 个缺少完整 workspaceBinding 的旧任务线程: ${skippedInvalidThreadIds.join(', ')}',
|
||||
'Skipped ${skippedInvalidThreadIds.length} persisted task threads missing a complete workspaceBinding: ${skippedInvalidThreadIds.join(', ')}',
|
||||
);
|
||||
if (disposedInternal) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -232,8 +232,19 @@ extension AppControllerDesktopSingleAgent on AppController {
|
||||
resolvedWorkspaceKind != WorkspaceRefKind.localPath) {
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
workspaceRef: resolvedWorkingDirectory,
|
||||
workspaceRefKind: resolvedWorkspaceKind,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: normalizedAssistantSessionKeyInternal(sessionKey),
|
||||
workspaceKind: resolvedWorkspaceKind == WorkspaceRefKind.remotePath
|
||||
? WorkspaceKind.remoteFs
|
||||
: WorkspaceKind.localFs,
|
||||
workspacePath: resolvedWorkingDirectory,
|
||||
displayPath: resolvedWorkingDirectory,
|
||||
writable:
|
||||
assistantThreadRecordsInternal[normalizedAssistantSessionKeyInternal(sessionKey)]
|
||||
?.workspaceBinding
|
||||
.writable ??
|
||||
true,
|
||||
),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -273,8 +273,6 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
ThreadSelectionSource? assistantModelSource,
|
||||
ThreadSelectionSource? selectedSkillsSource,
|
||||
String? gatewayEntryState,
|
||||
String? workspaceRef,
|
||||
WorkspaceRefKind? workspaceRefKind,
|
||||
}) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
@ -307,40 +305,13 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
subjectId: '',
|
||||
displayName: '',
|
||||
);
|
||||
final explicitWorkspaceKind = workspaceRefKind == null
|
||||
? null
|
||||
: (workspaceRefKind == WorkspaceRefKind.localPath
|
||||
? WorkspaceKind.localFs
|
||||
: WorkspaceKind.remoteFs);
|
||||
final baseWorkspaceBinding =
|
||||
workspaceBinding ??
|
||||
existing?.workspaceBinding ??
|
||||
WorkspaceBinding(
|
||||
workspaceId: normalizedSessionKey,
|
||||
workspaceKind:
|
||||
explicitWorkspaceKind ??
|
||||
(nextExecutionTarget == AssistantExecutionTarget.singleAgent
|
||||
? WorkspaceKind.localFs
|
||||
: WorkspaceKind.remoteFs),
|
||||
workspacePath: '',
|
||||
displayPath: '',
|
||||
writable: true,
|
||||
);
|
||||
final nextWorkspacePath =
|
||||
workspaceRef ?? baseWorkspaceBinding.workspacePath;
|
||||
final nextDisplayPath =
|
||||
workspaceRef ??
|
||||
(baseWorkspaceBinding.displayPath.trim().isNotEmpty
|
||||
? baseWorkspaceBinding.displayPath
|
||||
: nextWorkspacePath);
|
||||
final nextWorkspaceBinding = baseWorkspaceBinding.copyWith(
|
||||
workspaceId: baseWorkspaceBinding.workspaceId.trim().isEmpty
|
||||
? normalizedSessionKey
|
||||
: baseWorkspaceBinding.workspaceId,
|
||||
workspaceKind: explicitWorkspaceKind,
|
||||
workspacePath: nextWorkspacePath,
|
||||
displayPath: nextDisplayPath,
|
||||
);
|
||||
final nextWorkspaceBinding =
|
||||
workspaceBinding ?? existing?.workspaceBinding;
|
||||
if (nextWorkspaceBinding == null || !nextWorkspaceBinding.isComplete) {
|
||||
throw StateError(
|
||||
'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.',
|
||||
);
|
||||
}
|
||||
final nextProvider =
|
||||
singleAgentProvider ??
|
||||
SingleAgentProviderCopy.fromJsonValue(
|
||||
@ -410,11 +381,8 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
existing?.contextState.selectedSkillsSource,
|
||||
gatewayEntryState: gatewayEntryState,
|
||||
);
|
||||
final nextStatus = nextWorkspaceBinding.workspacePath.trim().isEmpty
|
||||
? 'needs_workspace'
|
||||
: (lifecycleState?.status ??
|
||||
existing?.lifecycleState.status ??
|
||||
'ready');
|
||||
final nextStatus =
|
||||
lifecycleState?.status ?? existing?.lifecycleState.status ?? 'ready';
|
||||
final nextLifecycleState =
|
||||
(lifecycleState ??
|
||||
existing?.lifecycleState ??
|
||||
|
||||
@ -544,6 +544,9 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
sessionKey,
|
||||
);
|
||||
final existing = assistantThreadRecordsInternal[normalizedSessionKey];
|
||||
if (existing == null || !existing.workspaceBinding.isComplete) {
|
||||
return;
|
||||
}
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
@ -551,16 +554,10 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: workspaceRoot,
|
||||
displayPath: workspaceRoot,
|
||||
writable: existing?.workspaceBinding.writable ?? true,
|
||||
writable: existing.workspaceBinding.writable,
|
||||
),
|
||||
lifecycleState:
|
||||
(existing?.lifecycleState ??
|
||||
const ThreadLifecycleState(
|
||||
archived: false,
|
||||
status: 'ready',
|
||||
lastRunAtMs: null,
|
||||
lastResultCode: null,
|
||||
))
|
||||
(existing.lifecycleState)
|
||||
.copyWith(status: 'ready'),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
@ -597,56 +594,5 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
Future<void> tryBindWorkspaceForOnlyChatFallbackInternal(
|
||||
String sessionKey,
|
||||
AssistantExecutionTarget currentTarget,
|
||||
) async {
|
||||
if (currentTarget != AssistantExecutionTarget.singleAgent &&
|
||||
currentTarget != AssistantExecutionTarget.auto) {
|
||||
return;
|
||||
}
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
if (!singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
|
||||
return;
|
||||
}
|
||||
if (assistantWorkspacePathForSession(normalizedSessionKey).trim().isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final candidateRoots = <String>{
|
||||
settings.workspacePath.trim(),
|
||||
Directory.current.path.trim(),
|
||||
settings.remoteProjectRoot.trim(),
|
||||
}.where((item) => item.isNotEmpty);
|
||||
for (final root in candidateRoots) {
|
||||
final threadWorkspace =
|
||||
'${trimTrailingPathSeparatorInternal(root)}/.xworkmate/threads/${threadWorkspaceDirectoryNameInternal(normalizedSessionKey)}';
|
||||
if (!ensureLocalWorkspaceDirectoryInternal(threadWorkspace)) {
|
||||
continue;
|
||||
}
|
||||
final existing = assistantThreadRecordsInternal[normalizedSessionKey];
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: normalizedSessionKey,
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: threadWorkspace,
|
||||
displayPath: threadWorkspace,
|
||||
writable: existing?.workspaceBinding.writable ?? true,
|
||||
),
|
||||
lifecycleState:
|
||||
(existing?.lifecycleState ??
|
||||
const ThreadLifecycleState(
|
||||
archived: false,
|
||||
status: 'ready',
|
||||
lastRunAtMs: null,
|
||||
lastResultCode: null,
|
||||
))
|
||||
.copyWith(status: 'ready'),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
recomputeTasksInternal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
) async {}
|
||||
}
|
||||
|
||||
@ -142,29 +142,16 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
if (executionTarget == AssistantExecutionTarget.auto ||
|
||||
executionTarget == AssistantExecutionTarget.singleAgent) {
|
||||
if (existingBinding != null &&
|
||||
existingBinding.workspacePath.trim().isNotEmpty) {
|
||||
if (existingBinding.workspaceKind == WorkspaceKind.localFs) {
|
||||
if (ensureLocalWorkspaceDirectoryInternal(
|
||||
existingBinding.workspacePath,
|
||||
)) {
|
||||
return existingBinding.copyWith(
|
||||
displayPath: existingBinding.workspacePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
final defaultRemotePath = remoteThreadWorkspacePathInternal(
|
||||
sessionKey,
|
||||
ownerScope,
|
||||
);
|
||||
if (existingBinding.workspacePath.trim() != defaultRemotePath) {
|
||||
return existingBinding.copyWith(
|
||||
displayPath: existingBinding.displayPath.trim().isEmpty
|
||||
? existingBinding.workspacePath
|
||||
: null,
|
||||
);
|
||||
}
|
||||
existingBinding.workspaceKind == WorkspaceKind.localFs &&
|
||||
ensureLocalWorkspaceDirectoryInternal(existingBinding.workspacePath)) {
|
||||
return existingBinding.copyWith(displayPath: existingBinding.workspacePath);
|
||||
}
|
||||
final localPath = localThreadWorkspacePathInternal(sessionKey);
|
||||
if (localPath.isEmpty) {
|
||||
throw StateError(
|
||||
'Local executable thread $sessionKey requires a writable local workspace.',
|
||||
);
|
||||
}
|
||||
return WorkspaceBinding(
|
||||
workspaceId: normalizedAssistantSessionKeyInternal(sessionKey),
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
@ -242,9 +229,6 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
ownerScope: ownerScope,
|
||||
existingBinding: existing?.workspaceBinding,
|
||||
);
|
||||
final lifecycleStatus = workspaceBinding.workspacePath.trim().isEmpty
|
||||
? 'needs_workspace'
|
||||
: 'ready';
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
ownerScope: ownerScope,
|
||||
@ -263,7 +247,7 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
lastRunAtMs: null,
|
||||
lastResultCode: null,
|
||||
))
|
||||
.copyWith(status: lifecycleStatus),
|
||||
.copyWith(status: 'ready'),
|
||||
executionTarget: resolvedExecutionTarget,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
|
||||
@ -144,21 +144,11 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final existing =
|
||||
assistantThreadRecordsInternal[normalizedSessionKey]
|
||||
return assistantThreadRecordsInternal[normalizedSessionKey]
|
||||
?.workspaceBinding
|
||||
.workspacePath
|
||||
.trim() ??
|
||||
'';
|
||||
if (existing.isNotEmpty) {
|
||||
return existing;
|
||||
}
|
||||
final target = assistantExecutionTargetForSession(normalizedSessionKey);
|
||||
if (target == AssistantExecutionTarget.singleAgent ||
|
||||
target == AssistantExecutionTarget.auto) {
|
||||
return localThreadWorkspacePathInternal(normalizedSessionKey);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
WorkspaceRefKind assistantWorkspaceKindForSession(String sessionKey) {
|
||||
|
||||
@ -669,9 +669,20 @@ extension AppControllerDesktopThreadStorage on AppController {
|
||||
if (sessionKey.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (!record.workspaceBinding.isComplete) {
|
||||
continue;
|
||||
}
|
||||
final titleFromSettings = assistantCustomTaskTitle(sessionKey);
|
||||
final workspaceBinding = record.workspaceBinding.copyWith(
|
||||
workspaceId: sessionKey,
|
||||
displayPath: record.workspaceKind == WorkspaceKind.localFs
|
||||
? record.workspacePath.trim()
|
||||
: (record.displayPath.trim().isEmpty
|
||||
? record.workspacePath.trim()
|
||||
: record.displayPath.trim()),
|
||||
);
|
||||
final normalizedRecord = record.copyWith(
|
||||
sessionKey: sessionKey,
|
||||
threadId: sessionKey,
|
||||
title: titleFromSettings.isEmpty
|
||||
? record.title.trim()
|
||||
: titleFromSettings,
|
||||
@ -690,16 +701,8 @@ extension AppControllerDesktopThreadStorage on AppController {
|
||||
gatewayEntryState: (record.gatewayEntryState ?? '').trim().isEmpty
|
||||
? gatewayEntryStateForTargetInternal(record.executionTarget)
|
||||
: record.gatewayEntryState,
|
||||
workspacePath: record.workspacePath.trim(),
|
||||
displayPath: record.workspaceKind == WorkspaceKind.localFs
|
||||
? record.workspacePath.trim()
|
||||
: (record.displayPath.trim().isEmpty
|
||||
? record.workspacePath.trim()
|
||||
: record.displayPath.trim()),
|
||||
workspaceKind: record.workspaceKind,
|
||||
lifecycleStatus: record.workspacePath.trim().isEmpty
|
||||
? 'needs_workspace'
|
||||
: record.lifecycleState.status,
|
||||
workspaceBinding: workspaceBinding,
|
||||
lifecycleState: record.lifecycleState.copyWith(status: 'ready'),
|
||||
);
|
||||
if (normalizedRecord.workspaceKind == WorkspaceKind.localFs &&
|
||||
normalizedRecord.workspacePath.trim().isNotEmpty) {
|
||||
|
||||
@ -60,16 +60,26 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
settings.assistantExecutionTarget == resolvedTarget) {
|
||||
return;
|
||||
}
|
||||
if (!assistantThreadRecordsInternal.containsKey(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
)) {
|
||||
initializeAssistantThreadContext(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
messageViewMode: currentAssistantMessageViewMode,
|
||||
singleAgentProvider: currentSingleAgentProvider,
|
||||
);
|
||||
}
|
||||
await ensureDesktopTaskThreadBindingInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
executionTargetSource: ThreadSelectionSource.explicit,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
await ensureDesktopTaskThreadBindingInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
await applyAssistantExecutionTargetInternal(
|
||||
@ -93,6 +103,14 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
if (singleAgentProviderForSession(sessionKey) == sanitizedProvider) {
|
||||
return;
|
||||
}
|
||||
if (!assistantThreadRecordsInternal.containsKey(sessionKey)) {
|
||||
initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: assistantExecutionTargetForSession(sessionKey),
|
||||
messageViewMode: assistantMessageViewModeForSession(sessionKey),
|
||||
singleAgentProvider: currentSingleAgentProvider,
|
||||
);
|
||||
}
|
||||
singleAgentRuntimeModelBySessionInternal.remove(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
@ -122,6 +140,14 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
if (assistantMessageViewModeForSession(sessionKey) == mode) {
|
||||
return;
|
||||
}
|
||||
if (!assistantThreadRecordsInternal.containsKey(sessionKey)) {
|
||||
initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: assistantExecutionTargetForSession(sessionKey),
|
||||
messageViewMode: assistantMessageViewModeForSession(sessionKey),
|
||||
singleAgentProvider: singleAgentProviderForSession(sessionKey),
|
||||
);
|
||||
}
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
messageViewMode: mode,
|
||||
@ -245,6 +271,14 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
trimmed) {
|
||||
return;
|
||||
}
|
||||
if (!assistantThreadRecordsInternal.containsKey(normalizedSessionKey)) {
|
||||
initializeAssistantThreadContext(
|
||||
normalizedSessionKey,
|
||||
executionTarget: assistantExecutionTargetForSession(normalizedSessionKey),
|
||||
messageViewMode: assistantMessageViewModeForSession(normalizedSessionKey),
|
||||
singleAgentProvider: singleAgentProviderForSession(normalizedSessionKey),
|
||||
);
|
||||
}
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
assistantModelId: trimmed,
|
||||
@ -281,24 +315,25 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
final resolvedTarget =
|
||||
executionTarget ??
|
||||
assistantExecutionTargetForSession(currentSessionKey);
|
||||
final initialWorkspaceBinding =
|
||||
resolvedTarget == AssistantExecutionTarget.singleAgent
|
||||
? (() {
|
||||
final localPath = localThreadWorkspacePathInternal(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
return WorkspaceBinding(
|
||||
workspaceId: normalizedSessionKey,
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: localPath,
|
||||
displayPath: localPath,
|
||||
writable: true,
|
||||
);
|
||||
})()
|
||||
: null;
|
||||
final initialOwnerScope =
|
||||
assistantThreadRecordsInternal[normalizedSessionKey]?.ownerScope ??
|
||||
const ThreadOwnerScope(
|
||||
realm: ThreadRealm.local,
|
||||
subjectType: ThreadSubjectType.user,
|
||||
subjectId: '',
|
||||
displayName: '',
|
||||
);
|
||||
final initialWorkspaceBinding = buildDesktopWorkspaceBindingInternal(
|
||||
normalizedSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
ownerScope: initialOwnerScope,
|
||||
existingBinding:
|
||||
assistantThreadRecordsInternal[normalizedSessionKey]?.workspaceBinding,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
title: title.trim(),
|
||||
ownerScope: initialOwnerScope,
|
||||
executionTarget: resolvedTarget,
|
||||
workspaceBinding: initialWorkspaceBinding,
|
||||
messageViewMode:
|
||||
|
||||
@ -247,7 +247,7 @@ extension AppControllerWebGatewayRelay on AppController {
|
||||
);
|
||||
final existing = threadRecordsInternal[resolvedKey];
|
||||
final next = (existing ?? newRecordInternal(target: target)).copyWith(
|
||||
sessionKey: resolvedKey,
|
||||
threadId: resolvedKey,
|
||||
messages: messages,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
title: deriveThreadTitleInternal(
|
||||
|
||||
@ -105,19 +105,22 @@ extension AppControllerWebHelpers on AppController {
|
||||
final target =
|
||||
sanitizeTargetInternal(record.executionTarget) ??
|
||||
AssistantExecutionTarget.singleAgent;
|
||||
final workspacePath = record.workspacePath.trim();
|
||||
return record.copyWith(
|
||||
executionTarget: target,
|
||||
title: record.title.trim().isEmpty
|
||||
? appText('新对话', 'New conversation')
|
||||
: record.title.trim(),
|
||||
workspacePath: record.workspacePath.trim(),
|
||||
displayPath: record.displayPath.trim().isEmpty
|
||||
? record.workspacePath.trim()
|
||||
: record.displayPath.trim(),
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
lifecycleStatus: record.workspacePath.trim().isEmpty
|
||||
? 'needs_workspace'
|
||||
: 'ready',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: record.threadId,
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: workspacePath,
|
||||
displayPath: record.displayPath.trim().isEmpty
|
||||
? workspacePath
|
||||
: record.displayPath.trim(),
|
||||
writable: record.workspaceBinding.writable,
|
||||
),
|
||||
lifecycleState: record.lifecycleState.copyWith(status: 'ready'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,6 +149,8 @@ extension AppControllerWebHelpers on AppController {
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
};
|
||||
final threadId = '$prefix:$timestamp';
|
||||
final workspacePath =
|
||||
'/owners/${ThreadRealm.remote.name}/${ThreadSubjectType.user.name}/threads/$threadId';
|
||||
return TaskThread(
|
||||
threadId: threadId,
|
||||
createdAtMs: timestamp.toDouble(),
|
||||
@ -160,8 +165,8 @@ extension AppControllerWebHelpers on AppController {
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: threadId,
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: '',
|
||||
displayPath: '',
|
||||
workspacePath: workspacePath,
|
||||
displayPath: workspacePath,
|
||||
writable: true,
|
||||
),
|
||||
executionBinding: ExecutionBinding(
|
||||
@ -188,7 +193,7 @@ extension AppControllerWebHelpers on AppController {
|
||||
),
|
||||
lifecycleState: const ThreadLifecycleState(
|
||||
archived: false,
|
||||
status: 'needs_workspace',
|
||||
status: 'ready',
|
||||
lastRunAtMs: null,
|
||||
lastResultCode: null,
|
||||
),
|
||||
@ -281,7 +286,7 @@ extension AppControllerWebHelpers on AppController {
|
||||
);
|
||||
threadRecordsInternal[key] =
|
||||
(existing ?? newRecordInternal(target: resolvedTarget)).copyWith(
|
||||
sessionKey: key,
|
||||
threadId: key,
|
||||
ownerScope: ownerScope,
|
||||
workspaceBinding: workspaceBinding,
|
||||
executionBinding: buildWebExecutionBindingInternal(
|
||||
@ -462,6 +467,12 @@ extension AppControllerWebHelpers on AppController {
|
||||
assistantExecutionTargetForSession(key);
|
||||
final existing =
|
||||
threadRecordsInternal[key] ?? newRecordInternal(target: resolvedTarget);
|
||||
final nextWorkspaceBinding = existing.workspaceBinding;
|
||||
if (!nextWorkspaceBinding.isComplete) {
|
||||
throw StateError(
|
||||
'TaskThread $key is missing a complete workspaceBinding.',
|
||||
);
|
||||
}
|
||||
threadRecordsInternal[key] = existing.copyWith(
|
||||
threadId: key,
|
||||
messages: messages ?? existing.messages,
|
||||
@ -485,8 +496,15 @@ extension AppControllerWebHelpers on AppController {
|
||||
selectedSkillsSource ?? existing.contextState.selectedSkillsSource,
|
||||
gatewayEntryState: gatewayEntryState ?? existing.gatewayEntryState,
|
||||
clearGatewayEntryState: clearGatewayEntryState,
|
||||
workspacePath: workspacePath ?? existing.workspacePath,
|
||||
workspaceKind: workspaceKind ?? existing.workspaceKind,
|
||||
workspaceBinding:
|
||||
(workspacePath != null || workspaceKind != null)
|
||||
? nextWorkspaceBinding.copyWith(
|
||||
workspacePath: workspacePath,
|
||||
displayPath: workspacePath ?? nextWorkspaceBinding.displayPath,
|
||||
workspaceKind: workspaceKind,
|
||||
)
|
||||
: nextWorkspaceBinding,
|
||||
lifecycleState: existing.lifecycleState.copyWith(status: 'ready'),
|
||||
);
|
||||
recomputeDerivedWorkspaceStateInternal();
|
||||
}
|
||||
|
||||
@ -97,48 +97,81 @@ class _AppShellState extends State<AppShell> {
|
||||
animation: widget.controller,
|
||||
builder: (context, _) {
|
||||
final controller = widget.controller;
|
||||
final palette = context.palette;
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final palette = context.palette;
|
||||
final platform = Theme.of(context).platform;
|
||||
final isCompactMobile =
|
||||
(platform == TargetPlatform.iOS ||
|
||||
platform == TargetPlatform.android) &&
|
||||
constraints.maxWidth < 900;
|
||||
final isMobile = constraints.maxWidth < 900;
|
||||
final sidebarState = controller.sidebarState;
|
||||
final showSidebar = sidebarState != AppSidebarState.hidden;
|
||||
final uiFeatures = controller.featuresFor(
|
||||
resolveUiFeaturePlatformFromContext(context),
|
||||
);
|
||||
final sidebarTaskItems = _buildSidebarTaskItems(controller);
|
||||
final expandedSidebarWidth = _clampSidebarWidth(
|
||||
_sidebarExpandedWidth ??
|
||||
_defaultSidebarWidth(
|
||||
controller.appLanguage,
|
||||
constraints.maxWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
if ((controller.startupTaskThreadWarning ?? '').trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.accentMuted,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: palette.warning),
|
||||
),
|
||||
constraints.maxWidth,
|
||||
);
|
||||
final showPinnedDetail =
|
||||
controller.detailPanel != null &&
|
||||
constraints.maxWidth > 1280;
|
||||
final mobileDestination =
|
||||
controller.destination == WorkspaceDestination.account
|
||||
? WorkspaceDestination.assistant
|
||||
: controller.destination;
|
||||
final availableMobileDestinations = _mobileDestinations
|
||||
.where(controller.capabilities.supportsDestination)
|
||||
.toList(growable: false);
|
||||
final resolvedMobileDestination =
|
||||
availableMobileDestinations.contains(mobileDestination)
|
||||
? mobileDestination
|
||||
: (availableMobileDestinations.isEmpty
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.startupTaskThreadWarning!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton(
|
||||
onPressed: controller.dismissStartupTaskThreadWarning,
|
||||
child: Text(appText('关闭', 'Dismiss')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final palette = context.palette;
|
||||
final platform = Theme.of(context).platform;
|
||||
final isCompactMobile =
|
||||
(platform == TargetPlatform.iOS ||
|
||||
platform == TargetPlatform.android) &&
|
||||
constraints.maxWidth < 900;
|
||||
final isMobile = constraints.maxWidth < 900;
|
||||
final sidebarState = controller.sidebarState;
|
||||
final showSidebar = sidebarState != AppSidebarState.hidden;
|
||||
final uiFeatures = controller.featuresFor(
|
||||
resolveUiFeaturePlatformFromContext(context),
|
||||
);
|
||||
final sidebarTaskItems = _buildSidebarTaskItems(controller);
|
||||
final expandedSidebarWidth = _clampSidebarWidth(
|
||||
_sidebarExpandedWidth ??
|
||||
_defaultSidebarWidth(
|
||||
controller.appLanguage,
|
||||
constraints.maxWidth,
|
||||
),
|
||||
constraints.maxWidth,
|
||||
);
|
||||
final showPinnedDetail =
|
||||
controller.detailPanel != null &&
|
||||
constraints.maxWidth > 1280;
|
||||
final mobileDestination =
|
||||
controller.destination == WorkspaceDestination.account
|
||||
? WorkspaceDestination.assistant
|
||||
: controller.destination;
|
||||
final availableMobileDestinations = _mobileDestinations
|
||||
.where(controller.capabilities.supportsDestination)
|
||||
.toList(growable: false);
|
||||
final resolvedMobileDestination =
|
||||
availableMobileDestinations.contains(mobileDestination)
|
||||
? mobileDestination
|
||||
: availableMobileDestinations.first);
|
||||
: (availableMobileDestinations.isEmpty
|
||||
? mobileDestination
|
||||
: availableMobileDestinations.first);
|
||||
|
||||
void openMobileDetail(DetailPanelData detail) {
|
||||
showModalBottomSheet<void>(
|
||||
@ -404,7 +437,10 @@ class _AppShellState extends State<AppShell> {
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -588,6 +588,9 @@ class WorkspaceBinding {
|
||||
final String displayPath;
|
||||
final bool writable;
|
||||
|
||||
bool get isComplete =>
|
||||
workspaceId.trim().isNotEmpty && workspacePath.trim().isNotEmpty;
|
||||
|
||||
WorkspaceBinding copyWith({
|
||||
String? workspaceId,
|
||||
WorkspaceKind? workspaceKind,
|
||||
@ -616,11 +619,17 @@ class WorkspaceBinding {
|
||||
|
||||
factory WorkspaceBinding.fromJson(Map<String, dynamic> json) {
|
||||
final path = json['workspacePath']?.toString() ?? '';
|
||||
final workspaceId = json['workspaceId']?.toString() ?? '';
|
||||
final workspaceKindValue = json['workspaceKind']?.toString();
|
||||
if (workspaceId.trim().isEmpty ||
|
||||
path.trim().isEmpty ||
|
||||
workspaceKindValue == null ||
|
||||
workspaceKindValue.trim().isEmpty) {
|
||||
throw const FormatException('TaskThread.workspaceBinding is incomplete.');
|
||||
}
|
||||
return WorkspaceBinding(
|
||||
workspaceId: json['workspaceId']?.toString() ?? '',
|
||||
workspaceKind: WorkspaceKindCopy.fromJsonValue(
|
||||
json['workspaceKind']?.toString(),
|
||||
),
|
||||
workspaceId: workspaceId,
|
||||
workspaceKind: WorkspaceKindCopy.fromJsonValue(workspaceKindValue),
|
||||
workspacePath: path,
|
||||
displayPath: json['displayPath']?.toString() ?? path,
|
||||
writable: json['writable'] as bool? ?? true,
|
||||
@ -877,11 +886,10 @@ class ThreadLifecycleState {
|
||||
|
||||
class TaskThread {
|
||||
TaskThread({
|
||||
String? threadId,
|
||||
String? sessionKey,
|
||||
required String threadId,
|
||||
String? title,
|
||||
ThreadOwnerScope? ownerScope,
|
||||
WorkspaceBinding? workspaceBinding,
|
||||
required WorkspaceBinding workspaceBinding,
|
||||
ExecutionBinding? executionBinding,
|
||||
ThreadContextState? contextState,
|
||||
ThreadLifecycleState? lifecycleState,
|
||||
@ -896,15 +904,11 @@ class TaskThread {
|
||||
String? assistantModelId,
|
||||
SingleAgentProvider? singleAgentProvider,
|
||||
String? gatewayEntryState,
|
||||
String? workspaceRef,
|
||||
WorkspaceRefKind? workspaceRefKind,
|
||||
String? displayPath,
|
||||
AssistantPermissionLevel? permissionLevel,
|
||||
String? latestResolvedRuntimeModel,
|
||||
String? lifecycleStatus,
|
||||
double? lastRunAtMs,
|
||||
String? lastResultCode,
|
||||
}) : threadId = _resolveThreadId(threadId, sessionKey),
|
||||
}) : threadId = _resolveThreadId(threadId),
|
||||
title = title ?? '',
|
||||
ownerScope =
|
||||
ownerScope ??
|
||||
@ -914,15 +918,7 @@ class TaskThread {
|
||||
subjectId: '',
|
||||
displayName: '',
|
||||
),
|
||||
workspaceBinding =
|
||||
workspaceBinding ??
|
||||
WorkspaceBinding(
|
||||
workspaceId: _resolveThreadId(threadId, sessionKey),
|
||||
workspaceKind: _workspaceKindFromLegacy(workspaceRefKind),
|
||||
workspacePath: workspaceRef?.trim() ?? '',
|
||||
displayPath: (displayPath ?? workspaceRef ?? '').trim(),
|
||||
writable: true,
|
||||
),
|
||||
workspaceBinding = _validateWorkspaceBinding(workspaceBinding),
|
||||
executionBinding =
|
||||
executionBinding ??
|
||||
ExecutionBinding(
|
||||
@ -953,11 +949,7 @@ class TaskThread {
|
||||
lifecycleState ??
|
||||
ThreadLifecycleState(
|
||||
archived: archived ?? false,
|
||||
status:
|
||||
lifecycleStatus ??
|
||||
((workspaceRef?.trim().isEmpty ?? true)
|
||||
? 'needs_workspace'
|
||||
: 'ready'),
|
||||
status: 'ready',
|
||||
lastRunAtMs: lastRunAtMs,
|
||||
lastResultCode: lastResultCode?.trim(),
|
||||
),
|
||||
@ -1016,7 +1008,6 @@ class TaskThread {
|
||||
|
||||
TaskThread copyWith({
|
||||
String? threadId,
|
||||
String? sessionKey,
|
||||
String? title,
|
||||
ThreadOwnerScope? ownerScope,
|
||||
WorkspaceBinding? workspaceBinding,
|
||||
@ -1040,13 +1031,6 @@ class TaskThread {
|
||||
ThreadSelectionSource? selectedSkillsSource,
|
||||
String? gatewayEntryState,
|
||||
bool clearGatewayEntryState = false,
|
||||
String? workspaceRef,
|
||||
WorkspaceRefKind? workspaceRefKind,
|
||||
String? workspacePath,
|
||||
String? displayPath,
|
||||
WorkspaceKind? workspaceKind,
|
||||
bool? writable,
|
||||
String? lifecycleStatus,
|
||||
String? latestResolvedRuntimeModel,
|
||||
}) {
|
||||
final nextExecutionBinding = executionBinding ?? this.executionBinding;
|
||||
@ -1063,19 +1047,10 @@ class TaskThread {
|
||||
ThreadExecutionMode.gatewayRemote,
|
||||
};
|
||||
return TaskThread(
|
||||
threadId: threadId ?? sessionKey ?? this.threadId,
|
||||
threadId: threadId ?? this.threadId,
|
||||
title: title ?? this.title,
|
||||
ownerScope: ownerScope ?? this.ownerScope,
|
||||
workspaceBinding: (workspaceBinding ?? this.workspaceBinding).copyWith(
|
||||
workspacePath: workspacePath ?? workspaceRef,
|
||||
displayPath: displayPath,
|
||||
workspaceKind:
|
||||
workspaceKind ??
|
||||
(workspaceRefKind == null
|
||||
? null
|
||||
: _workspaceKindFromLegacy(workspaceRefKind)),
|
||||
writable: writable,
|
||||
),
|
||||
workspaceBinding: workspaceBinding ?? this.workspaceBinding,
|
||||
executionBinding: nextExecutionBinding.copyWith(
|
||||
executionMode: nextExecutionMode,
|
||||
executorId: singleAgentProvider?.providerId,
|
||||
@ -1097,23 +1072,25 @@ class TaskThread {
|
||||
),
|
||||
lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith(
|
||||
archived: archived,
|
||||
status: lifecycleStatus,
|
||||
),
|
||||
createdAtMs: createdAtMs ?? this.createdAtMs,
|
||||
updatedAtMs: updatedAtMs ?? this.updatedAtMs,
|
||||
);
|
||||
}
|
||||
|
||||
static String _resolveThreadId(String? threadId, String? sessionKey) {
|
||||
return (threadId ?? sessionKey ?? '').trim();
|
||||
static String _resolveThreadId(String threadId) {
|
||||
return threadId.trim();
|
||||
}
|
||||
|
||||
static WorkspaceKind _workspaceKindFromLegacy(WorkspaceRefKind? kind) {
|
||||
return switch (kind) {
|
||||
WorkspaceRefKind.remotePath ||
|
||||
WorkspaceRefKind.objectStore => WorkspaceKind.remoteFs,
|
||||
_ => WorkspaceKind.localFs,
|
||||
};
|
||||
static WorkspaceBinding _validateWorkspaceBinding(
|
||||
WorkspaceBinding workspaceBinding,
|
||||
) {
|
||||
if (!workspaceBinding.isComplete) {
|
||||
throw StateError(
|
||||
'TaskThread requires a complete workspaceBinding at create/load time.',
|
||||
);
|
||||
}
|
||||
return workspaceBinding;
|
||||
}
|
||||
|
||||
static ThreadExecutionMode _executionModeFromLegacy(
|
||||
|
||||
@ -79,6 +79,9 @@ class SecureConfigStore {
|
||||
return _settingsStore.loadTaskThreads();
|
||||
}
|
||||
|
||||
List<String> get lastSkippedInvalidTaskThreadIds =>
|
||||
_settingsStore.lastSkippedInvalidTaskThreadIds;
|
||||
|
||||
Future<void> saveTaskThreads(List<TaskThread> records) {
|
||||
return _settingsStore.saveTaskThreads(records);
|
||||
}
|
||||
|
||||
@ -58,10 +58,13 @@ class SettingsStore {
|
||||
PersistentWriteFailure? _tasksWriteFailure;
|
||||
PersistentWriteFailure? _auditWriteFailure;
|
||||
bool _taskThreadStateResetRequired = false;
|
||||
List<String> _lastSkippedInvalidTaskThreadIds = const <String>[];
|
||||
|
||||
PersistentWriteFailure? get settingsWriteFailure => _settingsWriteFailure;
|
||||
PersistentWriteFailure? get tasksWriteFailure => _tasksWriteFailure;
|
||||
PersistentWriteFailure? get auditWriteFailure => _auditWriteFailure;
|
||||
List<String> get lastSkippedInvalidTaskThreadIds =>
|
||||
List<String>.unmodifiable(_lastSkippedInvalidTaskThreadIds);
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_initialized) {
|
||||
@ -426,8 +429,25 @@ class SettingsStore {
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
_taskThreadStateResetRequired = false;
|
||||
_lastSkippedInvalidTaskThreadIds = const <String>[];
|
||||
final orderedKeys = index.sessions;
|
||||
final recordsByKey = <String, TaskThread>{};
|
||||
final skippedIds = <String>{};
|
||||
|
||||
String inferThreadIdFromTaskFile(File file) {
|
||||
final name = file.uri.pathSegments.isEmpty
|
||||
? file.path
|
||||
: file.uri.pathSegments.last;
|
||||
final encoded = name.endsWith('.json')
|
||||
? name.substring(0, name.length - 5)
|
||||
: name;
|
||||
try {
|
||||
return utf8.decode(base64Url.decode(base64Url.normalize(encoded)));
|
||||
} catch (_) {
|
||||
return encoded;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await for (final entity in layout.tasksDirectory.list()) {
|
||||
if (entity is! File ||
|
||||
@ -452,6 +472,7 @@ class SettingsStore {
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
skippedIds.add(inferThreadIdFromTaskFile(entity));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -472,6 +493,7 @@ class SettingsStore {
|
||||
ordered.add(record);
|
||||
}
|
||||
}
|
||||
_lastSkippedInvalidTaskThreadIds = skippedIds.toList()..sort();
|
||||
return ordered;
|
||||
}
|
||||
|
||||
|
||||
@ -254,26 +254,36 @@ void registerAssistantPageSuiteComposerTestsInternal() {
|
||||
).create(recursive: true);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '${tempDirectory.path}/thread-main',
|
||||
displayPath: '${tempDirectory.path}/thread-main',
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: 'Main',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: '${tempDirectory.path}/thread-main',
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
TaskThread(
|
||||
sessionKey: 'draft:artifact-thread',
|
||||
threadId: 'draft:artifact-thread',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'draft:artifact-thread',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '${tempDirectory.path}/thread-task',
|
||||
displayPath: '${tempDirectory.path}/thread-task',
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 2,
|
||||
title: 'Artifact Thread',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: '${tempDirectory.path}/thread-task',
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
controller = CaptureSendAppControllerInternal(
|
||||
@ -823,7 +833,14 @@ void registerAssistantPageSuiteComposerTestsInternal() {
|
||||
final controller = await createControllerWithThreadRecordsInternal(
|
||||
records: <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '/tmp/main-thread',
|
||||
displayPath: '/tmp/main-thread',
|
||||
writable: true,
|
||||
),
|
||||
title: '研发任务',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
@ -937,7 +954,14 @@ void registerAssistantPageSuiteComposerTestsInternal() {
|
||||
final controller = await createControllerWithThreadRecordsInternal(
|
||||
records: <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '/tmp/main-thread',
|
||||
displayPath: '/tmp/main-thread',
|
||||
writable: true,
|
||||
),
|
||||
title: '研发任务',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
|
||||
@ -530,15 +530,20 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: threadWorkspace.path,
|
||||
displayPath: threadWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: 'Main',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: threadWorkspace.path,
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@ -89,15 +89,20 @@ void registerThreadSkillsAcpTests() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: workspaceRoot.path,
|
||||
displayPath: workspaceRoot.path,
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: '',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: workspaceRoot.path,
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
@ -281,15 +286,20 @@ void registerThreadSkillsAcpTests() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '${tempDirectory.path}/missing-workspace',
|
||||
displayPath: '${tempDirectory.path}/missing-workspace',
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: '',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: '${tempDirectory.path}/missing-workspace',
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@ -54,15 +54,20 @@ void registerThreadSkillsWorkspaceFallbackTests() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: workspaceRoot.path,
|
||||
displayPath: workspaceRoot.path,
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: '',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: workspaceRoot.path,
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
@ -146,15 +151,20 @@ void registerThreadSkillsWorkspaceFallbackTests() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: workspaceRoot.path,
|
||||
displayPath: workspaceRoot.path,
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: '',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: workspaceRoot.path,
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
@ -235,15 +245,20 @@ void registerThreadSkillsWorkspaceFallbackTests() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: workspaceRoot.path,
|
||||
displayPath: workspaceRoot.path,
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: '',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
workspaceRef: workspaceRoot.path,
|
||||
workspaceRefKind: WorkspaceRefKind.localPath,
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@ -58,7 +58,15 @@ void registerSecureConfigStoreSuiteCompatibilityTestsInternal() {
|
||||
);
|
||||
final legacyRecords = <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'draft:legacy-1',
|
||||
threadId: 'draft:legacy-1',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'draft:legacy-1',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: '/owners/remote/user/legacy/threads/draft:legacy-1',
|
||||
displayPath:
|
||||
'/owners/remote/user/legacy/threads/draft:legacy-1',
|
||||
writable: true,
|
||||
),
|
||||
title: 'Legacy thread',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.local,
|
||||
@ -255,52 +263,51 @@ void registerSecureConfigStoreSuiteCompatibilityTestsInternal() {
|
||||
expect(decoded.lifecycleState.status, 'ready');
|
||||
});
|
||||
|
||||
test('TaskThread defaults lifecycle status to needs_workspace', () {
|
||||
final decoded = TaskThread.fromJson(<String, dynamic>{
|
||||
'schemaVersion': taskThreadSchemaVersion,
|
||||
'threadId': 'thread-legacy',
|
||||
'title': 'Needs Workspace',
|
||||
'ownerScope': const <String, Object?>{
|
||||
'realm': 'local',
|
||||
'subjectType': 'user',
|
||||
'subjectId': 'device-1',
|
||||
'displayName': 'device-1',
|
||||
},
|
||||
'workspaceBinding': const <String, Object?>{
|
||||
'workspaceId': 'thread-legacy',
|
||||
'workspaceKind': 'localFs',
|
||||
'workspacePath': '',
|
||||
'displayPath': '',
|
||||
'writable': true,
|
||||
},
|
||||
'executionBinding': const <String, Object?>{
|
||||
'executionMode': 'localAgent',
|
||||
'executorId': 'auto',
|
||||
'providerId': 'auto',
|
||||
'endpointId': '',
|
||||
},
|
||||
'contextState': const <String, Object?>{
|
||||
'messages': <Object>[],
|
||||
'selectedModelId': '',
|
||||
'selectedSkillKeys': <Object>[],
|
||||
'importedSkills': <Object>[],
|
||||
'permissionLevel': 'defaultAccess',
|
||||
'messageViewMode': 'rendered',
|
||||
'latestResolvedRuntimeModel': '',
|
||||
},
|
||||
'lifecycleState': const <String, Object?>{
|
||||
'archived': false,
|
||||
'status': 'needs_workspace',
|
||||
'lastRunAtMs': null,
|
||||
'lastResultCode': null,
|
||||
},
|
||||
'createdAtMs': 1700000000000,
|
||||
'updatedAtMs': 1700000000000,
|
||||
});
|
||||
|
||||
expect(decoded.workspaceRef, isEmpty);
|
||||
expect(decoded.workspaceKind, WorkspaceKind.localFs);
|
||||
expect(decoded.lifecycleState.status, 'needs_workspace');
|
||||
test('TaskThread rejects persisted records without a complete binding', () {
|
||||
expect(
|
||||
() => TaskThread.fromJson(<String, dynamic>{
|
||||
'schemaVersion': taskThreadSchemaVersion,
|
||||
'threadId': 'thread-legacy',
|
||||
'title': 'Needs Workspace',
|
||||
'ownerScope': const <String, Object?>{
|
||||
'realm': 'local',
|
||||
'subjectType': 'user',
|
||||
'subjectId': 'device-1',
|
||||
'displayName': 'device-1',
|
||||
},
|
||||
'workspaceBinding': const <String, Object?>{
|
||||
'workspaceId': 'thread-legacy',
|
||||
'workspaceKind': 'localFs',
|
||||
'workspacePath': '',
|
||||
'displayPath': '',
|
||||
'writable': true,
|
||||
},
|
||||
'executionBinding': const <String, Object?>{
|
||||
'executionMode': 'localAgent',
|
||||
'executorId': 'auto',
|
||||
'providerId': 'auto',
|
||||
'endpointId': '',
|
||||
},
|
||||
'contextState': const <String, Object?>{
|
||||
'messages': <Object>[],
|
||||
'selectedModelId': '',
|
||||
'selectedSkillKeys': <Object>[],
|
||||
'importedSkills': <Object>[],
|
||||
'permissionLevel': 'defaultAccess',
|
||||
'messageViewMode': 'rendered',
|
||||
'latestResolvedRuntimeModel': '',
|
||||
},
|
||||
'lifecycleState': const <String, Object?>{
|
||||
'archived': false,
|
||||
'status': 'needs_workspace',
|
||||
'lastRunAtMs': null,
|
||||
'lastResultCode': null,
|
||||
},
|
||||
'createdAtMs': 1700000000000,
|
||||
'updatedAtMs': 1700000000000,
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -30,7 +30,14 @@ void registerSecureConfigStoreSuiteLifecycleTestsInternal() {
|
||||
);
|
||||
final records = <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'main',
|
||||
threadId: 'main',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'main',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: '/owners/remote/user/main/threads/main',
|
||||
displayPath: '/owners/remote/user/main/threads/main',
|
||||
writable: true,
|
||||
),
|
||||
title: '研发任务',
|
||||
archived: true,
|
||||
executionTarget: AssistantExecutionTarget.remote,
|
||||
@ -132,7 +139,14 @@ void registerSecureConfigStoreSuiteLifecycleTestsInternal() {
|
||||
);
|
||||
final records = <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'draft:backup-1',
|
||||
threadId: 'draft:backup-1',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'draft:backup-1',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: '/tmp/draft-backup-1',
|
||||
displayPath: '/tmp/draft-backup-1',
|
||||
writable: true,
|
||||
),
|
||||
title: '备份线程',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
@ -202,7 +216,15 @@ void registerSecureConfigStoreSuiteLifecycleTestsInternal() {
|
||||
);
|
||||
final records = <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'draft:clear-1',
|
||||
threadId: 'draft:clear-1',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'draft:clear-1',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: '/owners/remote/user/clear/threads/draft:clear-1',
|
||||
displayPath:
|
||||
'/owners/remote/user/clear/threads/draft:clear-1',
|
||||
writable: true,
|
||||
),
|
||||
title: '清理线程',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.local,
|
||||
|
||||
@ -187,7 +187,16 @@ void registerSecureConfigStoreSuiteSettingsTestsInternal() {
|
||||
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'draft:memory-only',
|
||||
threadId: 'draft:memory-only',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'draft:memory-only',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath:
|
||||
'/owners/remote/user/memory/threads/draft:memory-only',
|
||||
displayPath:
|
||||
'/owners/remote/user/memory/threads/draft:memory-only',
|
||||
writable: true,
|
||||
),
|
||||
title: 'Memory only',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.local,
|
||||
|
||||
@ -45,7 +45,14 @@ void main() {
|
||||
final bodies = <String>[];
|
||||
final records = <TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'direct:1',
|
||||
threadId: 'direct:1',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'direct:1',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath: '/owners/remote/user/direct/threads/direct:1',
|
||||
displayPath: '/owners/remote/user/direct/threads/direct:1',
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[
|
||||
GatewayChatMessage(
|
||||
id: 'm1',
|
||||
|
||||
@ -131,7 +131,16 @@ void main() {
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
TaskThread(
|
||||
sessionKey: 'direct:stale-browser-cache',
|
||||
threadId: 'direct:stale-browser-cache',
|
||||
workspaceBinding: const WorkspaceBinding(
|
||||
workspaceId: 'direct:stale-browser-cache',
|
||||
workspaceKind: WorkspaceKind.remoteFs,
|
||||
workspacePath:
|
||||
'/owners/remote/user/direct/threads/direct:stale-browser-cache',
|
||||
displayPath:
|
||||
'/owners/remote/user/direct/threads/direct:stale-browser-cache',
|
||||
writable: true,
|
||||
),
|
||||
messages: const <GatewayChatMessage>[],
|
||||
updatedAtMs: 1,
|
||||
title: 'stale browser cache',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user