@lesliechan721/aw 0.1.0-beta.25 → 0.1.0-beta.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/portable/commands.d.ts +39 -5
- package/dist/src/portable/commands.js +249 -30
- package/dist/src/portable/commands.js.map +1 -1
- package/dist/src/portable/skills-cli.d.ts +15 -7
- package/dist/src/portable/skills-cli.js +35 -19
- package/dist/src/portable/skills-cli.js.map +1 -1
- package/dist/src/tui/catalog-sources/session.js +9 -0
- package/dist/src/tui/catalog-sources/session.js.map +1 -1
- package/dist/src/tui/catalog-store-task.d.ts +16 -0
- package/dist/src/tui/catalog-store-task.js +19 -0
- package/dist/src/tui/catalog-store-task.js.map +1 -0
- package/dist/src/tui/components.d.ts +15 -0
- package/dist/src/tui/components.js +24 -3
- package/dist/src/tui/components.js.map +1 -1
- package/dist/src/tui/console-app-v4.d.ts +2 -1
- package/dist/src/tui/console-app-v4.js +2147 -1476
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +396 -16
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +131 -41
- package/dist/src/tui/controller.js +493 -33
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/dependency-graph.d.ts +11 -0
- package/dist/src/tui/dependency-graph.js +19 -0
- package/dist/src/tui/dependency-graph.js.map +1 -1
- package/dist/src/tui/diagnostic-projection.js +83 -0
- package/dist/src/tui/diagnostic-projection.js.map +1 -1
- package/dist/src/tui/model.d.ts +4 -1
- package/dist/src/tui/model.js +18 -13
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-diagnostic-screens.js +3 -2
- package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
- package/dist/src/tui/operation-review-planner.js +95 -8
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/plugin-content.d.ts +23 -0
- package/dist/src/tui/plugin-content.js +336 -0
- package/dist/src/tui/plugin-content.js.map +1 -0
- package/dist/src/tui/router.d.ts +27 -9
- package/dist/src/tui/router.js +13 -9
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +81 -14
- package/dist/src/tui/screen-registry.js +46 -17
- package/dist/src/tui/screen-registry.js.map +1 -1
- package/dist/src/tui/session-console-app.js +1 -1
- package/dist/src/tui/session-console-app.js.map +1 -1
- package/dist/src/tui/session-host.d.ts +1 -0
- package/dist/src/tui/session-host.js +13 -1
- package/dist/src/tui/session-host.js.map +1 -1
- package/dist/src/utils/codex-reconciler.js +5 -1
- package/dist/src/utils/codex-reconciler.js.map +1 -1
- package/dist/src/utils/codex-surface-analysis.d.ts +1 -0
- package/dist/src/utils/codex-surface-analysis.js +2 -0
- package/dist/src/utils/codex-surface-analysis.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
|
|
3
3
|
import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
|
|
4
|
-
import { flattenStatusTargets, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
|
|
4
|
+
import { flattenStatusTargets, statusTargetActivation, statusTargetExtensions, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
|
|
5
5
|
import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
|
|
6
6
|
import { buildOperationResultModel } from '../commands/operation-planner.js';
|
|
7
7
|
import { buildOperationReviewProjection } from './operation-review-planner.js';
|
|
@@ -86,6 +86,86 @@ function reviewPreparationStageLabel(stage) {
|
|
|
86
86
|
evidence: '准备确认信息',
|
|
87
87
|
}[stage];
|
|
88
88
|
}
|
|
89
|
+
function projectReminderTargetKey(target) {
|
|
90
|
+
return [target.pluginId, target.platform, target.scope].join('\0');
|
|
91
|
+
}
|
|
92
|
+
export function projectCurrentIntentReminder(status) {
|
|
93
|
+
const actionableTargets = [];
|
|
94
|
+
const blockedTargets = [];
|
|
95
|
+
const unconfirmedTargets = [];
|
|
96
|
+
const projectRoots = new Map((status.projectIntentRoots ?? []).map((root) => [
|
|
97
|
+
root.pluginId,
|
|
98
|
+
new Set(root.platforms),
|
|
99
|
+
]));
|
|
100
|
+
const projectTargets = flattenStatusTargets(status.payload.targets).filter((target) => {
|
|
101
|
+
const intent = statusTargetIntent(target);
|
|
102
|
+
return target.scope === 'project'
|
|
103
|
+
&& projectRoots.get(target.pluginId)?.has(target.platform) === true
|
|
104
|
+
&& intent.scopes.includes('project');
|
|
105
|
+
});
|
|
106
|
+
for (const target of projectTargets) {
|
|
107
|
+
if (target.state === 'ready')
|
|
108
|
+
continue;
|
|
109
|
+
const reminderTarget = {
|
|
110
|
+
pluginId: target.pluginId,
|
|
111
|
+
platform: target.platform,
|
|
112
|
+
scope: 'project',
|
|
113
|
+
};
|
|
114
|
+
const statusReasons = [
|
|
115
|
+
...target.reasons,
|
|
116
|
+
...(target.sharedReasons ?? []),
|
|
117
|
+
];
|
|
118
|
+
const reasonCodes = [...new Set(statusReasons.flatMap((reason) => [
|
|
119
|
+
reason.code,
|
|
120
|
+
...(reason.rootIssueCode ? [reason.rootIssueCode] : []),
|
|
121
|
+
]))].sort((left, right) => left.localeCompare(right, 'en'));
|
|
122
|
+
const sharedNative = statusTargetSharedNative(target);
|
|
123
|
+
const activation = statusTargetActivation(target);
|
|
124
|
+
const extensions = statusTargetExtensions(target);
|
|
125
|
+
const ownership = statusTargetOwnershipState(target);
|
|
126
|
+
const blocking = target.state === 'blocked'
|
|
127
|
+
|| [...target.reasons, ...(target.sharedReasons ?? [])]
|
|
128
|
+
.some((reason) => reason.severity === 'blocking');
|
|
129
|
+
const unobserved = ownership === 'error'
|
|
130
|
+
|| (!blocking && (sharedNative?.state === 'unobserved'
|
|
131
|
+
|| activation?.evidence === 'unobserved'
|
|
132
|
+
|| reasonCodes.includes('codexSharedNativeUnobserved')
|
|
133
|
+
|| target.components.some((component) => component.metadata?.rootIssueCode === 'codexSharedNativeUnobserved')));
|
|
134
|
+
const confirmedDifference = ownership === 'absent' || ownership === 'partial'
|
|
135
|
+
|| sharedNative?.state === 'missing' || sharedNative?.state === 'replaceable'
|
|
136
|
+
|| extensions?.state === 'missing' || extensions?.state === 'drifted'
|
|
137
|
+
|| activation?.evidence === 'observed' && activation.project === false
|
|
138
|
+
|| target.components.some((component) => component.kind === 'dependency'
|
|
139
|
+
&& (component.state === 'absent' || component.state === 'incomplete'));
|
|
140
|
+
if (blocking && confirmedDifference) {
|
|
141
|
+
blockedTargets.push({ ...reminderTarget, reasonCodes });
|
|
142
|
+
}
|
|
143
|
+
else if (!unobserved && target.state === 'needsSync' && confirmedDifference) {
|
|
144
|
+
actionableTargets.push({ ...reminderTarget, reasonCodes });
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
unconfirmedTargets.push(reminderTarget);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (projectTargets.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
const canonicalActionable = actionableTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
|
|
153
|
+
const canonicalBlocked = blockedTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
|
|
154
|
+
const canonicalUnconfirmed = unconfirmedTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
|
|
155
|
+
const projectRoot = status.projectRoot ?? projectTargets[0].cwd;
|
|
156
|
+
const signature = crypto.createHash('sha256').update(JSON.stringify({
|
|
157
|
+
projectRoot,
|
|
158
|
+
actionableTargets: canonicalActionable.map(projectReminderTargetKey),
|
|
159
|
+
blockedTargets: canonicalBlocked.map(projectReminderTargetKey),
|
|
160
|
+
})).digest('hex');
|
|
161
|
+
return {
|
|
162
|
+
projectRoot,
|
|
163
|
+
signature: `sha256:${signature}`,
|
|
164
|
+
actionableTargets: canonicalActionable,
|
|
165
|
+
blockedTargets: canonicalBlocked,
|
|
166
|
+
unconfirmedTargets: canonicalUnconfirmed,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
89
169
|
export class TuiOperationBlockedError extends Error {
|
|
90
170
|
code;
|
|
91
171
|
diagnostics;
|
|
@@ -140,10 +220,14 @@ function normalizeOperationIntent(intent) {
|
|
|
140
220
|
}
|
|
141
221
|
if (intent.kind !== 'pluginWriteBatch')
|
|
142
222
|
return intent;
|
|
143
|
-
if (intent.selection.kind === 'targets'
|
|
223
|
+
if (intent.selection.kind === 'targets'
|
|
224
|
+
|| intent.selection.kind === 'currentProjectIntentTargets') {
|
|
144
225
|
return {
|
|
145
226
|
...intent,
|
|
146
|
-
selection: {
|
|
227
|
+
selection: {
|
|
228
|
+
kind: intent.selection.kind,
|
|
229
|
+
targets: canonicalBatchTargets(intent.selection.targets),
|
|
230
|
+
},
|
|
147
231
|
};
|
|
148
232
|
}
|
|
149
233
|
if (intent.selection.kind !== 'selected')
|
|
@@ -352,9 +436,6 @@ export function pluginWriteTaskFailedRequestedTargets(execution) {
|
|
|
352
436
|
}));
|
|
353
437
|
}
|
|
354
438
|
function collectOperationSensitiveValues(intent) {
|
|
355
|
-
if (intent.kind !== 'configSave')
|
|
356
|
-
return [];
|
|
357
|
-
const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
|
|
358
439
|
const values = [];
|
|
359
440
|
const visit = (value) => {
|
|
360
441
|
if (typeof value === 'string')
|
|
@@ -364,9 +445,21 @@ function collectOperationSensitiveValues(intent) {
|
|
|
364
445
|
else if (value && typeof value === 'object')
|
|
365
446
|
Object.values(value).forEach(visit);
|
|
366
447
|
};
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
448
|
+
const collect = (operation) => {
|
|
449
|
+
const sensitiveKeys = new Set([
|
|
450
|
+
...(operation.sensitiveKeys ?? []),
|
|
451
|
+
...(operation.privateKeys ?? []),
|
|
452
|
+
]);
|
|
453
|
+
for (const [key, value] of Object.entries(operation.updates ?? {})) {
|
|
454
|
+
if (sensitiveKeys.has(key))
|
|
455
|
+
visit(value);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
if (intent.kind === 'configSave')
|
|
459
|
+
collect(intent);
|
|
460
|
+
else if (intent.kind === 'configSaveBatch') {
|
|
461
|
+
for (const group of intent.groups)
|
|
462
|
+
collect(group);
|
|
370
463
|
}
|
|
371
464
|
return values;
|
|
372
465
|
}
|
|
@@ -399,6 +492,14 @@ function cloneFrozen(value) {
|
|
|
399
492
|
return deepFreeze(structuredClone(value));
|
|
400
493
|
}
|
|
401
494
|
export function operationEvidenceFingerprint(evidence) {
|
|
495
|
+
if (evidence.catalogStore) {
|
|
496
|
+
return crypto.createHash('sha256')
|
|
497
|
+
.update(JSON.stringify(stableValue({
|
|
498
|
+
catalog: evidence.catalog,
|
|
499
|
+
authorizedItems: evidence.catalogStore.authorizedItems,
|
|
500
|
+
})))
|
|
501
|
+
.digest('hex');
|
|
502
|
+
}
|
|
402
503
|
if ('taskPlan' in evidence) {
|
|
403
504
|
const task = evidence.taskPlan;
|
|
404
505
|
return crypto.createHash('sha256')
|
|
@@ -470,10 +571,19 @@ export function operationEvidenceFingerprint(evidence) {
|
|
|
470
571
|
const config = staleEvidence.config
|
|
471
572
|
? (({ shareability: _shareability, ...identity }) => identity)(staleEvidence.config)
|
|
472
573
|
: undefined;
|
|
574
|
+
const configBatch = staleEvidence.configBatch
|
|
575
|
+
? {
|
|
576
|
+
groups: staleEvidence.configBatch.groups.map((group) => ({
|
|
577
|
+
...group,
|
|
578
|
+
config: (({ shareability: _shareability, ...identity }) => identity)(group.config),
|
|
579
|
+
})),
|
|
580
|
+
}
|
|
581
|
+
: undefined;
|
|
473
582
|
return crypto.createHash('sha256')
|
|
474
583
|
.update(JSON.stringify(stableValue({
|
|
475
584
|
...staleEvidence,
|
|
476
585
|
...(config ? { config } : {}),
|
|
586
|
+
...(configBatch ? { configBatch } : {}),
|
|
477
587
|
})))
|
|
478
588
|
.digest('hex');
|
|
479
589
|
}
|
|
@@ -545,6 +655,24 @@ function issueCode(error) {
|
|
|
545
655
|
const prefix = message.split(':', 1)[0];
|
|
546
656
|
return /^[A-Za-z][A-Za-z0-9]*$/u.test(prefix) ? prefix : 'operationBlocked';
|
|
547
657
|
}
|
|
658
|
+
function isPortableInstallReceipt(value) {
|
|
659
|
+
return typeof value === 'object' && value !== null
|
|
660
|
+
&& value.receiptSchemaVersion === 5
|
|
661
|
+
&& value.command === 'skill:install';
|
|
662
|
+
}
|
|
663
|
+
function completedSinglePluginId(intent, result) {
|
|
664
|
+
if (result.state !== 'succeeded' || result.summary.warnings?.length)
|
|
665
|
+
return undefined;
|
|
666
|
+
if (intent.kind === 'pluginWrite')
|
|
667
|
+
return intent.originIssue ? undefined : intent.pluginId;
|
|
668
|
+
const pluginIds = intent.kind === 'pluginWriteBatch'
|
|
669
|
+
? result.summary.batch?.requestedPluginIds ?? []
|
|
670
|
+
: intent.kind === 'pluginWriteTask'
|
|
671
|
+
? intent.targets.map((target) => target.pluginId)
|
|
672
|
+
: [];
|
|
673
|
+
const unique = [...new Set(pluginIds)];
|
|
674
|
+
return unique.length === 1 ? unique[0] : undefined;
|
|
675
|
+
}
|
|
548
676
|
export function extractPortableError(error) {
|
|
549
677
|
const typedCode = typeof error === 'object' && error !== null
|
|
550
678
|
&& 'code' in error && typeof error.code === 'string'
|
|
@@ -572,6 +700,9 @@ export class TuiController {
|
|
|
572
700
|
#reviewPreparationIntents = new Map();
|
|
573
701
|
#inflightOperations = new Map();
|
|
574
702
|
#pendingReads = new Set();
|
|
703
|
+
#pluginContent = new Map();
|
|
704
|
+
#pluginContentInflight = new Map();
|
|
705
|
+
#pluginContentEpoch = 0;
|
|
575
706
|
#abort = new AbortController();
|
|
576
707
|
#progressUnsubscribe;
|
|
577
708
|
#progressFeed;
|
|
@@ -591,6 +722,10 @@ export class TuiController {
|
|
|
591
722
|
#result;
|
|
592
723
|
#resultAnchor;
|
|
593
724
|
#portableResultSkills;
|
|
725
|
+
#portableResultReceipt;
|
|
726
|
+
#portableResultIntent;
|
|
727
|
+
#completionNotice;
|
|
728
|
+
#projectIntentReminder;
|
|
594
729
|
#loadError;
|
|
595
730
|
#portableDraft;
|
|
596
731
|
#recoverablePortableDraft;
|
|
@@ -700,6 +835,10 @@ export class TuiController {
|
|
|
700
835
|
: {}),
|
|
701
836
|
} }
|
|
702
837
|
: {}),
|
|
838
|
+
...(this.#completionNotice ? { completionNotice: this.#completionNotice } : {}),
|
|
839
|
+
...(this.#projectIntentReminder
|
|
840
|
+
? { projectIntentReminder: this.#projectIntentReminder }
|
|
841
|
+
: {}),
|
|
703
842
|
});
|
|
704
843
|
}
|
|
705
844
|
subscribe(listener) {
|
|
@@ -828,6 +967,7 @@ export class TuiController {
|
|
|
828
967
|
this.#result = undefined;
|
|
829
968
|
this.#resultAnchor = undefined;
|
|
830
969
|
this.#portableResultSkills = undefined;
|
|
970
|
+
this.#completionNotice = undefined;
|
|
831
971
|
this.#progress = { messages: [] };
|
|
832
972
|
this.#progressTargets = [];
|
|
833
973
|
this.#router.resetToRoot();
|
|
@@ -1022,6 +1162,7 @@ export class TuiController {
|
|
|
1022
1162
|
#project() {
|
|
1023
1163
|
if (!this.#status)
|
|
1024
1164
|
return;
|
|
1165
|
+
this.#projectIntentReminder = projectCurrentIntentReminder(this.#status);
|
|
1025
1166
|
this.#viewModel = buildTuiViewModel({
|
|
1026
1167
|
status: this.#status.payload,
|
|
1027
1168
|
pluginSummaries: this.#status.pluginSummaries,
|
|
@@ -1031,6 +1172,10 @@ export class TuiController {
|
|
|
1031
1172
|
catalogFreshness: this.#catalogFreshness,
|
|
1032
1173
|
catalogGenerations: this.#catalogGenerations,
|
|
1033
1174
|
dependencyOverviews: this.#status.dependencyOverviews,
|
|
1175
|
+
contentOverviews: {
|
|
1176
|
+
...(this.#status.contentOverviews ?? {}),
|
|
1177
|
+
...Object.fromEntries(this.#pluginContent),
|
|
1178
|
+
},
|
|
1034
1179
|
portablePluginIds: this.#status.portablePluginIds,
|
|
1035
1180
|
configStoreInventory: this.#status.configStoreInventory,
|
|
1036
1181
|
});
|
|
@@ -1088,7 +1233,13 @@ export class TuiController {
|
|
|
1088
1233
|
this.#router.beginPortableFlow(intent.pluginId);
|
|
1089
1234
|
return;
|
|
1090
1235
|
}
|
|
1091
|
-
if (intent.kind === '
|
|
1236
|
+
if (intent.kind === 'catalogStorePrune') {
|
|
1237
|
+
if (active?.kind === 'catalogStore')
|
|
1238
|
+
return;
|
|
1239
|
+
this.#router.beginFlow('catalogStore', { name: 'catalogStoreOverview' });
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (intent.kind === 'configSave' || intent.kind === 'configSaveBatch' || intent.kind === 'configReset') {
|
|
1092
1243
|
if (active?.kind === 'configEdit')
|
|
1093
1244
|
return;
|
|
1094
1245
|
const route = this.#router.snapshot.current.route;
|
|
@@ -1120,6 +1271,15 @@ export class TuiController {
|
|
|
1120
1271
|
}
|
|
1121
1272
|
#stableResultRoute(intent, result) {
|
|
1122
1273
|
const active = this.#router.activeFlow();
|
|
1274
|
+
if (intent.kind === 'pluginWriteTask' && active?.kind === 'pluginWriteTask'
|
|
1275
|
+
&& (active.parentFlowId || active.returnTarget.name === 'pluginWriteTask'
|
|
1276
|
+
&& active.returnTarget.goal !== intent.goal)) {
|
|
1277
|
+
return this.#normalizeRoute(active.returnTarget);
|
|
1278
|
+
}
|
|
1279
|
+
const completedPluginId = completedSinglePluginId(intent, result);
|
|
1280
|
+
if (completedPluginId) {
|
|
1281
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: completedPluginId });
|
|
1282
|
+
}
|
|
1123
1283
|
if (intent.kind === 'pluginWriteTask'
|
|
1124
1284
|
&& result.state === 'succeeded'
|
|
1125
1285
|
&& 'returnRoute' in result
|
|
@@ -1128,10 +1288,15 @@ export class TuiController {
|
|
|
1128
1288
|
&& result.returnRoute.batchOrigin?.owner === 'batch-write') {
|
|
1129
1289
|
return { name: 'pluginList', mode: 'installed' };
|
|
1130
1290
|
}
|
|
1291
|
+
if (intent.kind === 'portableInstall' && result.state === 'succeeded') {
|
|
1292
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
1293
|
+
}
|
|
1294
|
+
if (intent.kind === 'catalogStorePrune')
|
|
1295
|
+
return { name: 'catalogStoreOverview' };
|
|
1131
1296
|
if (active?.parentFlowId)
|
|
1132
1297
|
return this.#normalizeRoute(active.returnTarget);
|
|
1133
1298
|
if (active && (intent.kind === 'portableInstall'
|
|
1134
|
-
|| intent.kind === 'configSave' || intent.kind === 'configReset')) {
|
|
1299
|
+
|| intent.kind === 'configSave' || intent.kind === 'configSaveBatch' || intent.kind === 'configReset')) {
|
|
1135
1300
|
return this.#normalizeRoute(active.returnTarget);
|
|
1136
1301
|
}
|
|
1137
1302
|
if (intent.kind === 'pluginWrite') {
|
|
@@ -1139,11 +1304,18 @@ export class TuiController {
|
|
|
1139
1304
|
return { name: 'issueList' };
|
|
1140
1305
|
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
1141
1306
|
}
|
|
1307
|
+
if (intent.kind === 'pluginWriteBatch' && intent.selection.kind === 'targets') {
|
|
1308
|
+
const pluginIds = [...new Set(intent.selection.targets.map((target) => target.pluginId))];
|
|
1309
|
+
if (pluginIds.length === 1) {
|
|
1310
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: pluginIds[0] });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1142
1313
|
if (intent.kind === 'pluginWriteBatch')
|
|
1143
1314
|
return { name: 'pluginList', mode: 'installed' };
|
|
1144
1315
|
if (intent.kind === 'pluginWriteTask')
|
|
1145
1316
|
return { name: 'pluginList', mode: 'installed' };
|
|
1146
|
-
if (intent.kind === 'portableInstall' || intent.kind === 'configSave'
|
|
1317
|
+
if (intent.kind === 'portableInstall' || intent.kind === 'configSave'
|
|
1318
|
+
|| intent.kind === 'configSaveBatch' || intent.kind === 'configReset') {
|
|
1147
1319
|
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
1148
1320
|
}
|
|
1149
1321
|
if (intent.kind === 'sourceMutation' && intent.action === 'update') {
|
|
@@ -1188,7 +1360,9 @@ export class TuiController {
|
|
|
1188
1360
|
? {
|
|
1189
1361
|
...structuredClone(intent),
|
|
1190
1362
|
selection: {
|
|
1191
|
-
kind: '
|
|
1363
|
+
kind: intent.selection.kind === 'currentProjectIntentRoots'
|
|
1364
|
+
|| intent.selection.kind === 'currentProjectIntentTargets'
|
|
1365
|
+
? 'currentProjectIntentTargets' : 'targets',
|
|
1192
1366
|
targets: structuredClone(result.summary.batch.retryTargets),
|
|
1193
1367
|
},
|
|
1194
1368
|
}
|
|
@@ -1214,13 +1388,45 @@ export class TuiController {
|
|
|
1214
1388
|
&& result.returnRoute.batchOrigin?.owner === 'batch-write'
|
|
1215
1389
|
? result.returnRoute.batchOrigin.flowId
|
|
1216
1390
|
: undefined;
|
|
1391
|
+
const completedPluginId = completedSinglePluginId(intent, result);
|
|
1392
|
+
if (completedPluginId && target.name === 'pluginDetail') {
|
|
1393
|
+
this.#recordTerminalResult();
|
|
1394
|
+
const action = intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch'
|
|
1395
|
+
? intent.action : 'sync';
|
|
1396
|
+
this.#completionNotice = {
|
|
1397
|
+
pluginId: completedPluginId,
|
|
1398
|
+
title: intent.kind === 'pluginWriteTask'
|
|
1399
|
+
? intent.goal === 'update' ? '插件更新完成' : '插件恢复完成'
|
|
1400
|
+
: action === 'uninstall' ? '插件卸载完成'
|
|
1401
|
+
: action === 'install' ? '插件安装完成'
|
|
1402
|
+
: action === 'reinstall' ? '插件重新安装完成'
|
|
1403
|
+
: action === 'refreshAndSync' ? '插件更新完成'
|
|
1404
|
+
: action === 'cleanup' ? '插件清理完成' : '插件同步完成',
|
|
1405
|
+
description: action === 'uninstall'
|
|
1406
|
+
? '所选实际位置已处理。插件设置保持不变。'
|
|
1407
|
+
: '状态已重新检查。下方详情显示当前实际位置。',
|
|
1408
|
+
...(result.operationRecordId ? { operationRecordId: result.operationRecordId } : {}),
|
|
1409
|
+
};
|
|
1410
|
+
const finished = this.#router.finishFlow(target);
|
|
1411
|
+
if (!finished)
|
|
1412
|
+
this.#router.replace(target);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const portableParentFlowId = result.state === 'succeeded'
|
|
1416
|
+
&& intent.kind === 'portableInstall'
|
|
1417
|
+
&& this.#router.activeFlow()?.parentFlowId;
|
|
1217
1418
|
const finished = batchFlowId
|
|
1218
1419
|
? this.#router.finishFlowById(batchFlowId, 'batchWrite', target, terminal)
|
|
1219
|
-
:
|
|
1420
|
+
: portableParentFlowId
|
|
1421
|
+
? this.#router.finishFlowById(portableParentFlowId, 'pluginInstall', target, terminal)
|
|
1422
|
+
: this.#router.finishFlow(target, terminal);
|
|
1220
1423
|
if (!finished)
|
|
1221
1424
|
this.#router.replace(terminal);
|
|
1222
1425
|
}
|
|
1223
1426
|
#applyStatus(status) {
|
|
1427
|
+
this.#pluginContentEpoch += 1;
|
|
1428
|
+
this.#pluginContent.clear();
|
|
1429
|
+
this.#pluginContentInflight.clear();
|
|
1224
1430
|
this.#status = structuredClone(status);
|
|
1225
1431
|
this.#statusReadAtMs = this.dependencies.now?.() ?? Date.now();
|
|
1226
1432
|
if (status.catalogGenerations) {
|
|
@@ -1287,6 +1493,9 @@ export class TuiController {
|
|
|
1287
1493
|
this.#assertAcceptingActions();
|
|
1288
1494
|
this.dependencies.beginCatalogSession?.();
|
|
1289
1495
|
const generation = ++this.#generation;
|
|
1496
|
+
this.#pluginContentEpoch += 1;
|
|
1497
|
+
this.#pluginContent.clear();
|
|
1498
|
+
this.#pluginContentInflight.clear();
|
|
1290
1499
|
this.#phase = 'loading';
|
|
1291
1500
|
this.#publish();
|
|
1292
1501
|
let status;
|
|
@@ -1320,7 +1529,7 @@ export class TuiController {
|
|
|
1320
1529
|
&& this.#router.activeFlow()?.kind !== 'sourceEdit') {
|
|
1321
1530
|
this.#router.beginFlow('sourceEdit', this.#router.snapshot.current.route);
|
|
1322
1531
|
}
|
|
1323
|
-
this.#router.push(route, context);
|
|
1532
|
+
this.#router.push(route.name === 'pluginPreview' ? this.#normalizeRoute(route) : route, context);
|
|
1324
1533
|
this.#publish();
|
|
1325
1534
|
}
|
|
1326
1535
|
hasPortableSkills(pluginId) {
|
|
@@ -1439,6 +1648,14 @@ export class TuiController {
|
|
|
1439
1648
|
this.#portableDraft.canContinue = this.#portableDraft.totalCount > 0;
|
|
1440
1649
|
this.#publish();
|
|
1441
1650
|
}
|
|
1651
|
+
clearPortableSkills() {
|
|
1652
|
+
if (!this.#portableDraft)
|
|
1653
|
+
return;
|
|
1654
|
+
this.#portableDraft.candidates = this.#portableDraft.candidates.map((candidate) => ({ ...candidate, selected: false }));
|
|
1655
|
+
this.#portableDraft.selectedCount = 0;
|
|
1656
|
+
this.#portableDraft.canContinue = false;
|
|
1657
|
+
this.#publish();
|
|
1658
|
+
}
|
|
1442
1659
|
setPortableAgentMode(mode, requestedAgents = []) {
|
|
1443
1660
|
if (!this.#portableDraft)
|
|
1444
1661
|
return;
|
|
@@ -1483,6 +1700,63 @@ export class TuiController {
|
|
|
1483
1700
|
const intent = this.#portableInstallIntent();
|
|
1484
1701
|
return this.startReviewPreparation(intent, this.#router.portableOrigin(intent.pluginId));
|
|
1485
1702
|
}
|
|
1703
|
+
async recheckPortableResult() {
|
|
1704
|
+
this.#assertAcceptingActions();
|
|
1705
|
+
if (this.#router.snapshot.current.route.name !== 'result'
|
|
1706
|
+
|| this.#result?.operationKind !== 'portableInstall'
|
|
1707
|
+
|| !this.#portableResultReceipt
|
|
1708
|
+
|| !this.#portableResultIntent
|
|
1709
|
+
|| !this.dependencies.recheckPortableInstall) {
|
|
1710
|
+
throw new TuiOperationBlockedError('portableRecheckUnavailable', '当前结果不能执行只读复查。');
|
|
1711
|
+
}
|
|
1712
|
+
const checked = await this.#trackRead(this.dependencies.recheckPortableInstall(structuredClone(this.#portableResultReceipt), this.#abort.signal));
|
|
1713
|
+
this.#portableResultReceipt = structuredClone(checked);
|
|
1714
|
+
const summary = operationResultSummary(this.#portableResultIntent, checked);
|
|
1715
|
+
const previous = this.#result;
|
|
1716
|
+
this.#result = {
|
|
1717
|
+
state: checked.status === 'succeeded' ? 'succeeded' : 'partial',
|
|
1718
|
+
operationId: previous.operationId,
|
|
1719
|
+
summary: {
|
|
1720
|
+
...summary,
|
|
1721
|
+
elapsedMs: 'summary' in previous && previous.summary?.elapsedMs !== undefined
|
|
1722
|
+
? previous.summary.elapsedMs : summary.elapsedMs,
|
|
1723
|
+
},
|
|
1724
|
+
operationKind: 'portableInstall',
|
|
1725
|
+
pluginId: this.#portableResultIntent.pluginId,
|
|
1726
|
+
returnRoute: 'returnRoute' in previous && previous.returnRoute
|
|
1727
|
+
? structuredClone(previous.returnRoute)
|
|
1728
|
+
: { name: 'pluginDetail', pluginId: this.#portableResultIntent.pluginId },
|
|
1729
|
+
};
|
|
1730
|
+
this.#publish();
|
|
1731
|
+
return cloneFrozen(checked);
|
|
1732
|
+
}
|
|
1733
|
+
startPortableRetryReviewPreparation() {
|
|
1734
|
+
this.#assertAcceptingActions();
|
|
1735
|
+
const receipt = this.#portableResultReceipt;
|
|
1736
|
+
const original = this.#portableResultIntent;
|
|
1737
|
+
if (this.#router.snapshot.current.route.name !== 'result' || !receipt || !original) {
|
|
1738
|
+
throw new TuiOperationBlockedError('portableRetryUnavailable', '当前结果不能准备精确重试。');
|
|
1739
|
+
}
|
|
1740
|
+
if ((receipt.verification.unknown?.length ?? 0) > 0) {
|
|
1741
|
+
throw new TuiOperationBlockedError('portableRetryNotExact', '结果仍有未知目标。请先只读复查。');
|
|
1742
|
+
}
|
|
1743
|
+
const retryPairs = receipt.verification.missing;
|
|
1744
|
+
if (retryPairs.length === 0 || retryPairs.some((target) => !target.agent)) {
|
|
1745
|
+
throw new TuiOperationBlockedError('portableRetryNotExact', '没有可绑定到 Skill 与 Agent 的精确失败目标。');
|
|
1746
|
+
}
|
|
1747
|
+
const intent = {
|
|
1748
|
+
kind: 'portableInstall',
|
|
1749
|
+
pluginId: original.pluginId,
|
|
1750
|
+
scope: original.scope,
|
|
1751
|
+
selectedSkills: [...new Set(retryPairs.map((target) => target.skill))],
|
|
1752
|
+
requestedAgents: [...new Set(retryPairs.flatMap((target) => target.agent ? [target.agent] : []))],
|
|
1753
|
+
retry: { receipt: structuredClone(receipt) },
|
|
1754
|
+
};
|
|
1755
|
+
return this.startReviewPreparation(intent, {
|
|
1756
|
+
name: 'pluginDetail',
|
|
1757
|
+
pluginId: original.pluginId,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1486
1760
|
discardPortableDraft() {
|
|
1487
1761
|
this.#cancelPortableRequest();
|
|
1488
1762
|
this.#portableDraft = undefined;
|
|
@@ -1494,11 +1768,11 @@ export class TuiController {
|
|
|
1494
1768
|
this.#cancelInstallPlacementRequest();
|
|
1495
1769
|
const current = this.#router.snapshot.current.route;
|
|
1496
1770
|
if (current.name === 'portableBlocker' && current.code === 'requiredConfigMissing'
|
|
1497
|
-
&& route.name === '
|
|
1771
|
+
&& route.name === 'configFields' && route.pluginId === current.pluginId) {
|
|
1498
1772
|
this.#portableRepreparePluginId = current.pluginId;
|
|
1499
1773
|
}
|
|
1500
1774
|
this.#discardPortableDraftForRoute(route);
|
|
1501
|
-
this.#router.replace(route, context);
|
|
1775
|
+
this.#router.replace(route.name === 'pluginPreview' ? this.#normalizeRoute(route) : route, context);
|
|
1502
1776
|
this.#publish();
|
|
1503
1777
|
}
|
|
1504
1778
|
beginConfigFlow(pluginId) {
|
|
@@ -1660,6 +1934,7 @@ export class TuiController {
|
|
|
1660
1934
|
this.#result = undefined;
|
|
1661
1935
|
this.#resultAnchor = undefined;
|
|
1662
1936
|
this.#portableResultSkills = undefined;
|
|
1937
|
+
this.#completionNotice = undefined;
|
|
1663
1938
|
this.#publish();
|
|
1664
1939
|
return;
|
|
1665
1940
|
}
|
|
@@ -1671,7 +1946,8 @@ export class TuiController {
|
|
|
1671
1946
|
return;
|
|
1672
1947
|
}
|
|
1673
1948
|
if (this.#result?.state === 'succeeded'
|
|
1674
|
-
&& (this.#result.operationKind === 'configSave' || this.#result.operationKind === '
|
|
1949
|
+
&& (this.#result.operationKind === 'configSave' || this.#result.operationKind === 'configSaveBatch'
|
|
1950
|
+
|| this.#result.operationKind === 'configReset')
|
|
1675
1951
|
&& this.#result.pluginId
|
|
1676
1952
|
&& this.#router.resetConfigFlow(this.#result.pluginId, target)) {
|
|
1677
1953
|
this.#result = undefined;
|
|
@@ -1687,6 +1963,7 @@ export class TuiController {
|
|
|
1687
1963
|
this.#result = undefined;
|
|
1688
1964
|
this.#resultAnchor = undefined;
|
|
1689
1965
|
this.#portableResultSkills = undefined;
|
|
1966
|
+
this.#completionNotice = undefined;
|
|
1690
1967
|
this.#recoverableInstallPlacementDraft = undefined;
|
|
1691
1968
|
this.#recoverablePortableDraft = undefined;
|
|
1692
1969
|
this.#publish();
|
|
@@ -1698,7 +1975,8 @@ export class TuiController {
|
|
|
1698
1975
|
return;
|
|
1699
1976
|
const returnRoute = this.#normalizeRoute(result.returnRoute);
|
|
1700
1977
|
this.#router.back();
|
|
1701
|
-
const kind = result.operationKind === 'configSave' || result.operationKind === '
|
|
1978
|
+
const kind = result.operationKind === 'configSave' || result.operationKind === 'configSaveBatch'
|
|
1979
|
+
|| result.operationKind === 'configReset'
|
|
1702
1980
|
? 'configEdit'
|
|
1703
1981
|
: result.operationKind === 'portableInstall'
|
|
1704
1982
|
? 'portableInstall'
|
|
@@ -1728,6 +2006,7 @@ export class TuiController {
|
|
|
1728
2006
|
if (readOnlyHistoryRoute && this.#router.activeFlow()?.kind === 'portableInstall')
|
|
1729
2007
|
return;
|
|
1730
2008
|
const portableRoute = route.name === 'portableSkillSelect'
|
|
2009
|
+
|| route.name === 'portableSkillSummary'
|
|
1731
2010
|
|| route.name === 'portableDependencies'
|
|
1732
2011
|
|| route.name === 'portableAgentMode'
|
|
1733
2012
|
|| route.name === 'portableAgentInput'
|
|
@@ -1962,18 +2241,73 @@ export class TuiController {
|
|
|
1962
2241
|
}
|
|
1963
2242
|
listPlugins(options) {
|
|
1964
2243
|
const query = options.query?.trim().toLocaleLowerCase('en');
|
|
1965
|
-
const catalogPluginIds = new Set(this.#status?.pluginSummaries.map((plugin) => plugin.pluginId));
|
|
1966
2244
|
return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
|
|
1967
|
-
&& (options.
|
|
1968
|
-
|
|
2245
|
+
&& (options.installFilter !== 'installed'
|
|
2246
|
+
|| hasInstalledPlacement(plugin))
|
|
1969
2247
|
&& (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
|
|
1970
2248
|
&& (options.installFilter !== 'updateAvailable' || plugin.update?.state === 'available')
|
|
2249
|
+
&& (options.installFilter !== 'attention'
|
|
2250
|
+
|| plugin.aggregateStatus === 'needsAttention' || plugin.aggregateStatus === 'unavailable'
|
|
2251
|
+
|| plugin.primaryAttention !== undefined || plugin.sourceUnavailable === true
|
|
2252
|
+
|| plugin.capabilityUnsupported === true)
|
|
1971
2253
|
&& (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
|
|
1972
2254
|
}
|
|
1973
2255
|
getPluginOverview(pluginId) {
|
|
1974
2256
|
const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
|
|
1975
2257
|
return plugin ? cloneFrozen(plugin) : undefined;
|
|
1976
2258
|
}
|
|
2259
|
+
getPluginContentOverview(pluginId) {
|
|
2260
|
+
const overview = this.#viewModel?.contentOverviews[pluginId];
|
|
2261
|
+
return overview ? cloneFrozen(overview) : undefined;
|
|
2262
|
+
}
|
|
2263
|
+
getPluginContentState(pluginId) {
|
|
2264
|
+
if (this.#viewModel?.contentOverviews[pluginId])
|
|
2265
|
+
return 'ready';
|
|
2266
|
+
return this.#pluginContentInflight.has(pluginId) ? 'loading' : 'idle';
|
|
2267
|
+
}
|
|
2268
|
+
loadPluginContent(pluginId) {
|
|
2269
|
+
if (this.#viewModel?.contentOverviews[pluginId])
|
|
2270
|
+
return Promise.resolve();
|
|
2271
|
+
const current = this.#pluginContentInflight.get(pluginId);
|
|
2272
|
+
if (current)
|
|
2273
|
+
return current;
|
|
2274
|
+
const loader = this.dependencies.loadPluginContent;
|
|
2275
|
+
if (!loader)
|
|
2276
|
+
return Promise.resolve();
|
|
2277
|
+
const generation = this.#generation;
|
|
2278
|
+
const contentEpoch = this.#pluginContentEpoch;
|
|
2279
|
+
const promise = this.#trackRead(loader(pluginId, this.#abort.signal))
|
|
2280
|
+
.then((overview) => {
|
|
2281
|
+
if (this.#disposed || generation !== this.#generation
|
|
2282
|
+
|| contentEpoch !== this.#pluginContentEpoch)
|
|
2283
|
+
return;
|
|
2284
|
+
this.#pluginContent.set(pluginId, structuredClone(overview));
|
|
2285
|
+
this.#project();
|
|
2286
|
+
})
|
|
2287
|
+
.catch(() => {
|
|
2288
|
+
if (this.#disposed || generation !== this.#generation
|
|
2289
|
+
|| contentEpoch !== this.#pluginContentEpoch)
|
|
2290
|
+
return;
|
|
2291
|
+
this.#pluginContent.set(pluginId, {
|
|
2292
|
+
pluginId,
|
|
2293
|
+
pluginVersion: '待确认',
|
|
2294
|
+
catalogVersion: '待确认',
|
|
2295
|
+
evidence: 'unavailable',
|
|
2296
|
+
items: [],
|
|
2297
|
+
notices: ['当前无法安全读取这个插件的内容。'],
|
|
2298
|
+
});
|
|
2299
|
+
this.#project();
|
|
2300
|
+
})
|
|
2301
|
+
.finally(() => {
|
|
2302
|
+
if (this.#pluginContentInflight.get(pluginId) !== promise)
|
|
2303
|
+
return;
|
|
2304
|
+
this.#pluginContentInflight.delete(pluginId);
|
|
2305
|
+
this.#publish();
|
|
2306
|
+
});
|
|
2307
|
+
this.#pluginContentInflight.set(pluginId, promise);
|
|
2308
|
+
this.#publish();
|
|
2309
|
+
return promise;
|
|
2310
|
+
}
|
|
1977
2311
|
getPluginSourceIdentity(pluginId) {
|
|
1978
2312
|
return flattenStatusTargets(this.#status?.payload.targets ?? []).find((target) => target.pluginId === pluginId)?.sourceIdentity;
|
|
1979
2313
|
}
|
|
@@ -2207,7 +2541,9 @@ export class TuiController {
|
|
|
2207
2541
|
this.#publish();
|
|
2208
2542
|
}
|
|
2209
2543
|
startPluginWriteBatchReview(intent) {
|
|
2210
|
-
return this.startReviewPreparation(intent,
|
|
2544
|
+
return this.startReviewPreparation(intent, intent.action === 'install'
|
|
2545
|
+
? { name: 'batchInstallScope' }
|
|
2546
|
+
: { name: 'batchPlacement' });
|
|
2211
2547
|
}
|
|
2212
2548
|
startPluginWriteTaskReview(intent, returnRoute) {
|
|
2213
2549
|
return this.startReviewPreparation(intent, returnRoute);
|
|
@@ -2347,6 +2683,13 @@ export class TuiController {
|
|
|
2347
2683
|
}
|
|
2348
2684
|
return cloneFrozen(await this.#trackRead(this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal)));
|
|
2349
2685
|
}
|
|
2686
|
+
async inspectCatalogStore() {
|
|
2687
|
+
this.#assertAcceptingActions();
|
|
2688
|
+
if (!this.dependencies.inspectCatalogStore) {
|
|
2689
|
+
throw new TuiOperationBlockedError('catalogStoreUnavailable', 'Catalog 本地存储检查不可用。');
|
|
2690
|
+
}
|
|
2691
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.inspectCatalogStore(this.#abort.signal)));
|
|
2692
|
+
}
|
|
2350
2693
|
async getConfigGroups(pluginId, layer) {
|
|
2351
2694
|
this.#assertAcceptingActions();
|
|
2352
2695
|
if (!this.dependencies.getConfigGroups) {
|
|
@@ -2412,6 +2755,7 @@ export class TuiController {
|
|
|
2412
2755
|
}
|
|
2413
2756
|
startReviewPreparation(intent, returnRoute) {
|
|
2414
2757
|
this.#assertAcceptingActions();
|
|
2758
|
+
this.#completionNotice = undefined;
|
|
2415
2759
|
intent = normalizeOperationIntent(intent);
|
|
2416
2760
|
if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
|
|
2417
2761
|
const blockReason = this.catalogSourceRecoveryBlockReason(intent);
|
|
@@ -2492,10 +2836,25 @@ export class TuiController {
|
|
|
2492
2836
|
}
|
|
2493
2837
|
}
|
|
2494
2838
|
if (intent.kind === 'pluginWriteBatch') {
|
|
2495
|
-
if (intent.selection.kind === 'currentProjectIntentRoots'
|
|
2839
|
+
if ((intent.selection.kind === 'currentProjectIntentRoots'
|
|
2840
|
+
|| intent.selection.kind === 'currentProjectIntentTargets')
|
|
2496
2841
|
&& (intent.action !== 'sync' || intent.scope !== 'project')) {
|
|
2497
2842
|
throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目全部同步只支持 project scope sync。');
|
|
2498
2843
|
}
|
|
2844
|
+
if (intent.selection.kind === 'currentProjectIntentTargets') {
|
|
2845
|
+
const targets = intent.selection.targets;
|
|
2846
|
+
const canonical = [...new Map(targets.map((target) => [
|
|
2847
|
+
projectReminderTargetKey(target), target,
|
|
2848
|
+
])).values()].sort((left, right) => projectReminderTargetKey(left)
|
|
2849
|
+
.localeCompare(projectReminderTargetKey(right), 'en'));
|
|
2850
|
+
if (targets.length === 0
|
|
2851
|
+
|| targets.some((target) => target.scope !== 'project')
|
|
2852
|
+
|| JSON.stringify(targets) !== JSON.stringify(canonical)) {
|
|
2853
|
+
throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目同步目标必须是非空、规范且精确的 project 目标集合。');
|
|
2854
|
+
}
|
|
2855
|
+
for (const target of targets)
|
|
2856
|
+
sourceIdFromPluginId(target.pluginId);
|
|
2857
|
+
}
|
|
2499
2858
|
if (intent.selection.kind === 'selected') {
|
|
2500
2859
|
const canonical = canonicalPluginIds(intent.selection.pluginIds);
|
|
2501
2860
|
if (canonical.length === 0) {
|
|
@@ -2513,6 +2872,8 @@ export class TuiController {
|
|
|
2513
2872
|
#reviewPreparationLabel(intent) {
|
|
2514
2873
|
if (intent.kind === 'catalogSourcesRecovery')
|
|
2515
2874
|
return 'Catalog 来源配置';
|
|
2875
|
+
if (intent.kind === 'catalogStorePrune')
|
|
2876
|
+
return 'Catalog 本地存储';
|
|
2516
2877
|
if (intent.kind === 'pluginWriteTask')
|
|
2517
2878
|
return `${intent.targets.length} 个安装位置`;
|
|
2518
2879
|
if (intent.kind === 'pluginWriteBatch') {
|
|
@@ -2686,7 +3047,8 @@ export class TuiController {
|
|
|
2686
3047
|
}
|
|
2687
3048
|
async #logOperationError(error, code, intent) {
|
|
2688
3049
|
await this.#logErrorSafe(error, code, {
|
|
2689
|
-
privateConfigOperation: intent.kind === 'configSave' || intent.kind === '
|
|
3050
|
+
privateConfigOperation: intent.kind === 'configSave' || intent.kind === 'configSaveBatch'
|
|
3051
|
+
|| intent.kind === 'configReset'
|
|
2690
3052
|
|| intent.kind === 'sourceMutation',
|
|
2691
3053
|
});
|
|
2692
3054
|
}
|
|
@@ -2863,7 +3225,8 @@ export class TuiController {
|
|
|
2863
3225
|
: ticket.intent.kind === 'pluginWriteTask'
|
|
2864
3226
|
? `${ticket.evidence.taskPlan.executableTargets.length} 个安装位置`
|
|
2865
3227
|
: 'pluginId' in ticket.intent ? ticket.intent.pluginId
|
|
2866
|
-
: 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理'
|
|
3228
|
+
: 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理'
|
|
3229
|
+
: ticket.intent.kind === 'catalogStorePrune' ? 'Catalog 本地存储' : 'Catalog 来源配置',
|
|
2867
3230
|
phase: '验证当前状态',
|
|
2868
3231
|
},
|
|
2869
3232
|
};
|
|
@@ -2932,6 +3295,10 @@ export class TuiController {
|
|
|
2932
3295
|
const execution = ticket.intent.kind === 'catalogGeneration'
|
|
2933
3296
|
? await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence), this.#abort.signal)
|
|
2934
3297
|
: await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence));
|
|
3298
|
+
if (ticket.intent.kind === 'portableInstall' && isPortableInstallReceipt(execution)) {
|
|
3299
|
+
this.#portableResultReceipt = structuredClone(execution);
|
|
3300
|
+
this.#portableResultIntent = structuredClone(ticket.intent);
|
|
3301
|
+
}
|
|
2935
3302
|
const executionProjection = projectExecutionDiagnostics(execution, `${this.#sessionEpoch}:${operationId}`, collectOperationSensitiveValues(ticket.intent), ticket.intent.kind);
|
|
2936
3303
|
this.#operationDiagnostics.set(operationId, {
|
|
2937
3304
|
...executionProjection,
|
|
@@ -2989,8 +3356,12 @@ export class TuiController {
|
|
|
2989
3356
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2990
3357
|
const configCommitted = typeof execution === 'object'
|
|
2991
3358
|
&& execution !== null
|
|
2992
|
-
&& execution.configWrite === 'succeeded'
|
|
2993
|
-
|
|
3359
|
+
&& (execution.configWrite === 'succeeded'
|
|
3360
|
+
|| Array.isArray(execution.groups)
|
|
3361
|
+
&& (execution.groups
|
|
3362
|
+
.some((group) => group.configSaved)));
|
|
3363
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configSaveBatch'
|
|
3364
|
+
|| ticket.intent.kind === 'configReset')
|
|
2994
3365
|
&& configCommitted) {
|
|
2995
3366
|
const baseSummary = operationResultSummary(ticket.intent, execution);
|
|
2996
3367
|
const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
|
|
@@ -3095,6 +3466,7 @@ export class TuiController {
|
|
|
3095
3466
|
navigation: 'result',
|
|
3096
3467
|
returnRoute: ticket.returnRoute,
|
|
3097
3468
|
...(ticket.intent.kind === 'pluginWriteBatch' || ticket.intent.kind === 'pluginWriteTask'
|
|
3469
|
+
|| ticket.intent.kind === 'pluginWrite' && ticket.intent.action === 'uninstall'
|
|
3098
3470
|
|| dependencyFailure || dependencyStillRequired
|
|
3099
3471
|
? { summary: pluginSummary } : {}),
|
|
3100
3472
|
...(ticket.intent.kind !== 'pluginWriteBatch' && ticket.intent.kind !== 'pluginWriteTask'
|
|
@@ -3149,6 +3521,15 @@ export class TuiController {
|
|
|
3149
3521
|
this.#publish();
|
|
3150
3522
|
return cloneFrozen(result);
|
|
3151
3523
|
}
|
|
3524
|
+
if (ticket.intent.kind === 'configSaveBatch') {
|
|
3525
|
+
return this.#setBlocked(operationId, 'operationFailed', {
|
|
3526
|
+
intent: ticket.intent,
|
|
3527
|
+
navigation: 'result',
|
|
3528
|
+
returnRoute: ticket.returnRoute,
|
|
3529
|
+
summary,
|
|
3530
|
+
writeMayHaveCompleted: false,
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3152
3533
|
return this.#setBlocked(operationId, 'operationFailed', {
|
|
3153
3534
|
intent: ticket.intent,
|
|
3154
3535
|
navigation: 'result',
|
|
@@ -3176,8 +3557,13 @@ export class TuiController {
|
|
|
3176
3557
|
: { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
|
|
3177
3558
|
: ticket.returnRoute)),
|
|
3178
3559
|
};
|
|
3179
|
-
|
|
3180
|
-
|
|
3560
|
+
const configResultNeedsAttention = result.state === 'succeeded' && (result.summary.messages?.some((message) => message.startsWith('警告:')
|
|
3561
|
+
|| message === '提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。') ?? false);
|
|
3562
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configSaveBatch'
|
|
3563
|
+
|| ticket.intent.kind === 'configReset')
|
|
3564
|
+
&& this.#portableRepreparePluginId === ticket.intent.pluginId
|
|
3565
|
+
&& result.state === 'succeeded'
|
|
3566
|
+
&& !configResultNeedsAttention) {
|
|
3181
3567
|
this.#operationDiagnostics.delete(operationId);
|
|
3182
3568
|
this.#portableRepreparePluginId = undefined;
|
|
3183
3569
|
this.#activeReview = undefined;
|
|
@@ -3471,6 +3857,20 @@ export class TuiController {
|
|
|
3471
3857
|
}
|
|
3472
3858
|
}
|
|
3473
3859
|
function operationResultSummaryUnsafe(intent, execution) {
|
|
3860
|
+
if (intent.kind === 'catalogStorePrune') {
|
|
3861
|
+
const result = execution;
|
|
3862
|
+
return {
|
|
3863
|
+
code: result.overallState === 'succeeded' ? 'operationSucceeded' : 'operationPartial',
|
|
3864
|
+
message: result.overallState === 'succeeded'
|
|
3865
|
+
? `Catalog 本地存储清理完成;删除 ${result.deleted.length} 项。`
|
|
3866
|
+
: `Catalog 本地存储部分清理完成;删除 ${result.deleted.length} 项。`,
|
|
3867
|
+
messages: [
|
|
3868
|
+
result.deletedBytes === null ? '已删除大小未知。' : `已删除 ${result.deletedBytes} bytes。`,
|
|
3869
|
+
`保留 ${result.retained.length} 项。`,
|
|
3870
|
+
...(result.errors.length > 0 ? [`${result.errors.length} 项未能安全删除。`] : []),
|
|
3871
|
+
],
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3474
3874
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
3475
3875
|
const result = execution;
|
|
3476
3876
|
return {
|
|
@@ -3519,9 +3919,10 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
3519
3919
|
}
|
|
3520
3920
|
if (intent.kind === 'portableInstall') {
|
|
3521
3921
|
const receipt = execution;
|
|
3522
|
-
const verificationUnknown = receipt.
|
|
3523
|
-
|
|
3524
|
-
|
|
3922
|
+
const verificationUnknown = (receipt.verification.unknown?.length ?? 0) > 0
|
|
3923
|
+
|| receipt.failures.some((failure) => failure.code === 'portableSkillsCliVerificationFailed'
|
|
3924
|
+
&& receipt.verification.records.length === 0
|
|
3925
|
+
&& receipt.verification.missing.length === 0);
|
|
3525
3926
|
return {
|
|
3526
3927
|
code: receipt.status === 'succeeded' ? 'operationSucceeded'
|
|
3527
3928
|
: receipt.status === 'partial' ? 'operationPartial' : 'operationFailed',
|
|
@@ -3540,7 +3941,9 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
3540
3941
|
scope: receipt.scope,
|
|
3541
3942
|
materializationDigest: receipt.materializationDigest,
|
|
3542
3943
|
skillsCliVersion: receipt.skillsCliVersion,
|
|
3944
|
+
verified: receipt.verification.records,
|
|
3543
3945
|
missing: receipt.verification.missing,
|
|
3946
|
+
unknown: receipt.verification.unknown ?? [],
|
|
3544
3947
|
verificationUnknown,
|
|
3545
3948
|
...(receipt.failures[0]?.recovery ? { recovery: receipt.failures[0].recovery } : {}),
|
|
3546
3949
|
...(receipt.failures[0]?.code ? { failureCode: receipt.failures[0].code } : {}),
|
|
@@ -3805,6 +4208,62 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
3805
4208
|
})),
|
|
3806
4209
|
};
|
|
3807
4210
|
}
|
|
4211
|
+
if (intent.kind === 'configSaveBatch') {
|
|
4212
|
+
const batch = execution;
|
|
4213
|
+
const savedCount = batch.groups.filter((group) => group.configSaved).length;
|
|
4214
|
+
const failedCount = batch.groups.filter((group) => group.status === 'failed').length;
|
|
4215
|
+
const retryScopes = batch.groups.filter((group) => group.configSaved && group.applyStatus === 'failed' && group.errorCode !== 'invalidConfig');
|
|
4216
|
+
const invalidConfig = savedCount === batch.groups.length
|
|
4217
|
+
&& batch.groups.some((group) => group.configSaved && group.errorCode === 'invalidConfig')
|
|
4218
|
+
&& retryScopes.length === 0;
|
|
4219
|
+
const publicWarnings = [...new Set(batch.groups.flatMap((group) => {
|
|
4220
|
+
if (!group.execution || typeof group.execution !== 'object'
|
|
4221
|
+
|| !Array.isArray(group.execution.warnings))
|
|
4222
|
+
return [];
|
|
4223
|
+
return group.execution.warnings.filter((warning) => typeof warning === 'string' && warning.startsWith('projectConfigShareabilityUnverified:'));
|
|
4224
|
+
}))];
|
|
4225
|
+
const otherProjectsMayRequireSync = batch.groups.some((group) => group.scope === 'user' && group.execution && typeof group.execution === 'object'
|
|
4226
|
+
&& group.execution.otherProjectsMayRequireSync === true);
|
|
4227
|
+
return {
|
|
4228
|
+
code: failedCount > 0
|
|
4229
|
+
? savedCount > 0 ? 'operationPartial' : 'operationFailed'
|
|
4230
|
+
: 'operationSucceeded',
|
|
4231
|
+
message: invalidConfig
|
|
4232
|
+
? '设置已保存,但没有应用。当前配置仍无效。'
|
|
4233
|
+
: failedCount > 0
|
|
4234
|
+
? savedCount > 0
|
|
4235
|
+
? `已保存 ${savedCount} 个设置层;${failedCount} 个设置层失败。`
|
|
4236
|
+
: '设置没有保存。'
|
|
4237
|
+
: '设置已保存。',
|
|
4238
|
+
pluginId: intent.pluginId,
|
|
4239
|
+
configApplyRetryAvailable: retryScopes.length > 0,
|
|
4240
|
+
...(invalidConfig ? { configApplySkippedReason: 'invalidConfig' } : {}),
|
|
4241
|
+
configApplied: intent.apply
|
|
4242
|
+
? batch.groups.some((group) => group.applyStatus === 'succeeded')
|
|
4243
|
+
: false,
|
|
4244
|
+
configGroups: batch.groups.map((group) => ({
|
|
4245
|
+
scope: group.scope,
|
|
4246
|
+
configSaved: group.configSaved,
|
|
4247
|
+
applyRequested: group.applyRequested,
|
|
4248
|
+
applyStatus: group.applyStatus,
|
|
4249
|
+
...(group.errorCode ? { errorCode: safeDiagnosticCode(group.errorCode, 'configWriteFailed') } : {}),
|
|
4250
|
+
})),
|
|
4251
|
+
messages: [
|
|
4252
|
+
...batch.groups.map((group) => {
|
|
4253
|
+
const label = group.scope === 'project' ? '当前项目' : '个人默认';
|
|
4254
|
+
return group.configSaved
|
|
4255
|
+
? `${label}已保存${group.errorCode === 'invalidConfig'
|
|
4256
|
+
? ',但当前配置无效,未应用'
|
|
4257
|
+
: group.applyStatus === 'failed' ? ',但应用失败' : ''}。`
|
|
4258
|
+
: `${label}保存失败。`;
|
|
4259
|
+
}),
|
|
4260
|
+
...publicWarnings.map((warning) => `警告:${warning}`),
|
|
4261
|
+
...(otherProjectsMayRequireSync
|
|
4262
|
+
? ['提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。']
|
|
4263
|
+
: []),
|
|
4264
|
+
],
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
3808
4267
|
if (intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
3809
4268
|
const rawApply = typeof execution === 'object' && execution !== null
|
|
3810
4269
|
&& typeof execution.apply === 'object'
|
|
@@ -3878,6 +4337,7 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
3878
4337
|
configScope: intent.scope,
|
|
3879
4338
|
configApplyRetryAvailable: intent.kind === 'configSave'
|
|
3880
4339
|
&& intent.apply && !invalidConfig && (applyState === 'partial' || applyState === 'unavailable'),
|
|
4340
|
+
...(intent.kind === 'configSave' && intent.retryApply ? { configRetryApply: true } : {}),
|
|
3881
4341
|
...(publicMessages.length > 0 ? { messages: publicMessages } : {}),
|
|
3882
4342
|
...(intent.kind === 'configSave'
|
|
3883
4343
|
? { configApplied: applyState === 'succeeded' || applyState === 'partial' }
|