@huaqiu/dsh-tool-plugin-update 0.6.6 → 0.6.8

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.
@@ -21,7 +21,15 @@ import {
21
21
  observeHostLocale,
22
22
  type HostLocale,
23
23
  } from '@huaqiu/dsh-sidebar-entry'
24
- import { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'
24
+ import {
25
+ checkUpdates,
26
+ fetchJobStatus,
27
+ fetchReport,
28
+ TERMINAL_PHASES,
29
+ type PluginUpdateReport,
30
+ type UpdateJob,
31
+ type UpdateJobStatus,
32
+ } from './api.js'
25
33
  import { COPY } from './copy.js'
26
34
  import { PLUGIN_ICON } from './icons.js'
27
35
  import { openUpdateView, type UpdateStoreLike } from './update-view.js'
@@ -767,6 +775,97 @@ const VIEW_CSS_TEXT = `
767
775
  padding: 0 24px 20px;
768
776
  }
769
777
 
778
+ /* ── Durable job (active / result) ────────────────────────────────────── */
779
+ .hqpu-job {
780
+ display: flex;
781
+ flex-direction: column;
782
+ gap: 6px;
783
+ padding: 12px;
784
+ border: 0.5px solid var(--dsw-alias-border-l2);
785
+ border-radius: var(--dsw-radius-lg);
786
+ background: var(--dsw-alias-bg-layer-3);
787
+ }
788
+ .hqpu-job-meta {
789
+ font-variant-numeric: tabular-nums;
790
+ }
791
+ .hqpu-progress {
792
+ height: 6px;
793
+ border-radius: 999px;
794
+ background: var(--dsw-alias-bg-module-platform);
795
+ overflow: hidden;
796
+ }
797
+ .hqpu-progress-fill {
798
+ height: 100%;
799
+ border-radius: 999px;
800
+ background: var(--dsw-alias-state-business-primary);
801
+ transition: width var(--ds-transition-duration, 0.2s) ease;
802
+ }
803
+ .hqpu-job-steps {
804
+ display: flex;
805
+ flex-direction: column;
806
+ gap: 4px;
807
+ }
808
+ .hqpu-steps {
809
+ margin: 0;
810
+ padding: 0;
811
+ list-style: none;
812
+ display: flex;
813
+ flex-direction: column;
814
+ gap: 4px;
815
+ }
816
+ .hqpu-step {
817
+ display: flex;
818
+ align-items: center;
819
+ gap: 6px;
820
+ font-size: 12.5px;
821
+ line-height: 18px;
822
+ color: var(--dsw-alias-label-secondary);
823
+ }
824
+ .hqpu-step--skipped,
825
+ .hqpu-step--pending { opacity: 0.6; }
826
+
827
+ /* ── Per-plugin change list + release notes ────────────────────────────── */
828
+ .hqpu-changes {
829
+ padding: 0 8px 8px 70px;
830
+ }
831
+ .hqpu-changes-list,
832
+ .hqpu-release-notes {
833
+ margin: 4px 0 0;
834
+ padding-left: 18px;
835
+ font-size: 12.5px;
836
+ line-height: 19px;
837
+ color: var(--dsw-alias-label-secondary);
838
+ }
839
+ .hqpu-release-link {
840
+ align-self: flex-start;
841
+ }
842
+
843
+ /* ── Advanced disclosure (source editors) ───────────────────────────────── */
844
+ .hqpu-advanced > summary {
845
+ cursor: pointer;
846
+ font-size: 12.5px;
847
+ line-height: 20px;
848
+ color: var(--dsw-alias-label-tertiary);
849
+ list-style: none;
850
+ padding: 2px 0;
851
+ }
852
+ .hqpu-advanced > summary::-webkit-details-marker { display: none; }
853
+ .hqpu-advanced > summary::before {
854
+ content: '▸ ';
855
+ }
856
+ .hqpu-advanced[open] > summary::before {
857
+ content: '▾ ';
858
+ }
859
+
860
+ /* Danger-toned banner (check failure, breaking changes) */
861
+ .hqpu-banner[data-tone='danger'] {
862
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
863
+ color: var(--dsw-alias-label-secondary);
864
+ }
865
+ .hqpu-banner[data-tone='danger'] > svg {
866
+ color: var(--dsw-alias-state-error-primary);
867
+ }
868
+
770
869
  .hqpu-visually-hidden {
771
870
  position: absolute;
772
871
  width: 1px;
@@ -785,10 +884,649 @@ const VIEW_CSS_TEXT = `
785
884
  .hqpu-panel,
786
885
  .hqpu-skeleton-fill,
787
886
  .hqpu-spinner,
788
- .hqpu-toast {
887
+ .hqpu-toast,
888
+ .hqpu-pulse-ring,
889
+ .hqpu-job-step--active {
789
890
  animation: none;
790
891
  }
791
892
  }
893
+
894
+ /* ════════════════════════════════════════════════════════════════════════
895
+ * Refactored dock (interactive_refactored_ui_mockup.html) — a narrow dark-zinc
896
+ * update-center column. These rules cascade over the shell-token base: the
897
+ * panel becomes ~340px wide, the zinc palette hard-coded per mockup, with a
898
+ * sticky header / scroll body / sticky footer. No Tailwind, no dependencies.
899
+ * ════════════════════════════════════════════════════════════════════════ */
900
+ .hqpu-panel,
901
+ .hqpu-panel * {
902
+ --hq-bg: #18181b;
903
+ --hq-card: #27272a;
904
+ --hq-card-soft: #1c1c1f;
905
+ --hq-border: #3f3f46;
906
+ --hq-hover: #323236;
907
+ --hq-text: #f4f4f5;
908
+ --hq-muted: #a1a1aa;
909
+ --hq-faint: #71717a;
910
+ --hq-brand: #dc2626;
911
+ --hq-brand-soft: #ef4444;
912
+ --hq-emerald: #34d399;
913
+ --hq-amber: #f59e0b;
914
+ }
915
+ .hqpu-panel {
916
+ width: min(360px, calc(100vw - 32px));
917
+ max-height: min(86vh, 720px);
918
+ background: var(--hq-bg);
919
+ color: var(--hq-text);
920
+ border: 0.5px solid var(--hq-border);
921
+ border-radius: 12px;
922
+ gap: 0;
923
+ overflow: hidden;
924
+ }
925
+ .hqpu-scrim {
926
+ background: rgba(0, 0, 0, 0.55);
927
+ }
928
+
929
+ /* Sticky header */
930
+ .hqpu-head {
931
+ flex: none;
932
+ display: flex;
933
+ align-items: center;
934
+ justify-content: space-between;
935
+ gap: 8px;
936
+ padding: 10px 12px;
937
+ border-bottom: 0.5px solid var(--hq-border);
938
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
939
+ position: sticky;
940
+ top: 0;
941
+ z-index: 2;
942
+ }
943
+ .hqpu-head-main {
944
+ display: flex;
945
+ align-items: center;
946
+ gap: 8px;
947
+ min-width: 0;
948
+ margin: 0;
949
+ }
950
+ .hqpu-brand-icon {
951
+ display: flex;
952
+ align-items: center;
953
+ justify-content: center;
954
+ flex: none;
955
+ width: 24px;
956
+ height: 24px;
957
+ border-radius: 6px;
958
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
959
+ color: var(--hq-brand-soft);
960
+ }
961
+ .hqpu-brand-icon svg { width: 14px; height: 14px; display: block; }
962
+ .hqpu-title-block { min-width: 0; }
963
+ .hqpu-title {
964
+ margin: 0;
965
+ font-size: 13px;
966
+ font-weight: 600;
967
+ line-height: 16px;
968
+ color: var(--hq-text);
969
+ }
970
+ .hqpu-sub {
971
+ margin: 2px 0 0;
972
+ font-size: 10px;
973
+ line-height: 14px;
974
+ color: var(--hq-muted);
975
+ white-space: nowrap;
976
+ overflow: hidden;
977
+ text-overflow: ellipsis;
978
+ }
979
+ .hqpu-toolbar { gap: 2px; }
980
+ .hqpu-icon-btn {
981
+ width: 26px;
982
+ height: 26px;
983
+ border-radius: 6px;
984
+ color: var(--hq-muted);
985
+ }
986
+ .hqpu-icon-btn:hover:not(:disabled) {
987
+ background: var(--hq-hover);
988
+ color: var(--hq-text);
989
+ }
990
+ .hqpu-icon-btn svg { width: 14px; height: 14px; }
991
+
992
+ /* Collapsible settings panel */
993
+ .hqpu-settings {
994
+ flex: none;
995
+ padding: 10px 12px;
996
+ border-bottom: 0.5px solid var(--hq-border);
997
+ background: #131316;
998
+ }
999
+ .hqpu-settings-row {
1000
+ display: flex;
1001
+ align-items: center;
1002
+ justify-content: space-between;
1003
+ gap: 8px;
1004
+ margin-bottom: 8px;
1005
+ }
1006
+ .hqpu-settings-title {
1007
+ font-size: 11px;
1008
+ font-weight: 500;
1009
+ color: #d4d4d8;
1010
+ }
1011
+ .hqpu-origin-badge {
1012
+ font-size: 9px;
1013
+ padding: 1px 6px;
1014
+ border-radius: 4px;
1015
+ border: 0.5px solid;
1016
+ }
1017
+ .hqpu-origin-badge[data-origin='config'] {
1018
+ background: rgba(6, 78, 59, 0.4);
1019
+ color: var(--hq-emerald);
1020
+ border-color: rgba(6, 95, 70, 0.6);
1021
+ }
1022
+ .hqpu-origin-badge[data-origin='override'] {
1023
+ background: rgba(120, 53, 15, 0.4);
1024
+ color: var(--hq-amber);
1025
+ border-color: rgba(180, 83, 9, 0.5);
1026
+ }
1027
+ .hqpu-settings label {
1028
+ display: block;
1029
+ font-size: 10px;
1030
+ color: var(--hq-muted);
1031
+ margin: 6px 0 3px;
1032
+ }
1033
+ .hqpu-source-input {
1034
+ width: 100%;
1035
+ height: 26px;
1036
+ font-size: 10px;
1037
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1038
+ background: var(--hq-bg);
1039
+ border: 0.5px solid var(--hq-border);
1040
+ border-radius: 5px;
1041
+ color: #d4d4d8;
1042
+ padding: 0 8px;
1043
+ box-sizing: border-box;
1044
+ }
1045
+ .hqpu-source-input:focus-visible {
1046
+ outline: none;
1047
+ border-color: var(--hq-brand-soft);
1048
+ }
1049
+ .hqpu-settings-actions {
1050
+ display: flex;
1051
+ justify-content: flex-end;
1052
+ gap: 6px;
1053
+ margin-top: 8px;
1054
+ }
1055
+
1056
+ /* Scroll body */
1057
+ .hqpu-scroll {
1058
+ flex: 1;
1059
+ padding: 12px;
1060
+ background: var(--hq-bg);
1061
+ --dsh-scrollbar-thumb: #3f3f46;
1062
+ --dsh-scrollbar-thumb-hover: #52525b;
1063
+ }
1064
+ .hqpu-column {
1065
+ max-width: none;
1066
+ margin: 0;
1067
+ gap: 10px;
1068
+ }
1069
+ .hqpu-stack { gap: 10px; }
1070
+
1071
+ /* ── Active-job banner ─────────────────────────────────────────────────── */
1072
+ .hqpu-active {
1073
+ position: relative;
1074
+ padding: 12px;
1075
+ border-radius: 8px;
1076
+ border: 0.5px solid color-mix(in srgb, var(--hq-brand-soft) 30%, transparent);
1077
+ background: #18181b;
1078
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
1079
+ }
1080
+ .hqpu-active-top {
1081
+ display: flex;
1082
+ align-items: center;
1083
+ justify-content: space-between;
1084
+ gap: 8px;
1085
+ }
1086
+ .hqpu-active-title {
1087
+ display: flex;
1088
+ align-items: center;
1089
+ gap: 6px;
1090
+ min-width: 0;
1091
+ font-size: 11px;
1092
+ font-weight: 600;
1093
+ color: #e4e4e7;
1094
+ }
1095
+ .hqpu-pulse-dot {
1096
+ position: relative;
1097
+ flex: none;
1098
+ width: 8px;
1099
+ height: 8px;
1100
+ }
1101
+ .hqpu-pulse-dot::before {
1102
+ content: '';
1103
+ position: absolute;
1104
+ inset: 0;
1105
+ border-radius: 50%;
1106
+ background: var(--hq-brand-soft);
1107
+ }
1108
+ .hqpu-pulse-ring {
1109
+ position: absolute;
1110
+ inset: 0;
1111
+ border-radius: 50%;
1112
+ background: var(--hq-brand-soft);
1113
+ opacity: 0.6;
1114
+ animation: hqpu-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
1115
+ }
1116
+ @keyframes hqpu-ping {
1117
+ 75%, 100% { transform: scale(2.2); opacity: 0; }
1118
+ }
1119
+ .hqpu-active-percent {
1120
+ flex: none;
1121
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1122
+ font-size: 11px;
1123
+ font-weight: 500;
1124
+ color: var(--hq-brand-soft);
1125
+ }
1126
+ .hqpu-progress {
1127
+ height: 6px;
1128
+ margin-top: 10px;
1129
+ border-radius: 999px;
1130
+ background: #27272a;
1131
+ overflow: hidden;
1132
+ }
1133
+ .hqpu-progress-fill {
1134
+ background: linear-gradient(to right, var(--hq-brand), var(--hq-brand-soft));
1135
+ transition: width 0.3s ease;
1136
+ }
1137
+ .hqpu-active-meta {
1138
+ display: flex;
1139
+ justify-content: space-between;
1140
+ gap: 8px;
1141
+ margin-top: 4px;
1142
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1143
+ font-size: 10px;
1144
+ color: var(--hq-muted);
1145
+ }
1146
+ /* 4-step indicator row */
1147
+ .hqpu-job-steps4 {
1148
+ display: grid;
1149
+ grid-template-columns: repeat(4, 1fr);
1150
+ gap: 4px;
1151
+ margin-top: 10px;
1152
+ padding-top: 8px;
1153
+ border-top: 0.5px solid #27272a;
1154
+ text-align: center;
1155
+ font-size: 9px;
1156
+ font-weight: 500;
1157
+ }
1158
+ .hqpu-job-step--done { color: var(--hq-emerald); }
1159
+ .hqpu-job-step--active {
1160
+ color: var(--hq-brand-soft);
1161
+ animation: hqpu-step-pulse 1.2s ease-in-out infinite;
1162
+ }
1163
+ .hqpu-job-step--todo { color: var(--hq-muted); }
1164
+ @keyframes hqpu-step-pulse {
1165
+ 0%, 100% { opacity: 1; }
1166
+ 50% { opacity: 0.45; }
1167
+ }
1168
+
1169
+ /* ── Result / rollback alert (amber) vs green success ──────────────────── */
1170
+ .hqpu-alert {
1171
+ display: flex;
1172
+ align-items: flex-start;
1173
+ gap: 8px;
1174
+ padding: 10px;
1175
+ border-radius: 8px;
1176
+ }
1177
+ .hqpu-alert--amber {
1178
+ background: rgba(69, 26, 3, 0.35);
1179
+ border: 0.5px solid rgba(180, 83, 9, 0.4);
1180
+ }
1181
+ .hqpu-alert--amber svg { color: var(--hq-amber); flex: none; margin-top: 1px; }
1182
+ .hqpu-alert-green {
1183
+ background: rgba(6, 78, 59, 0.3);
1184
+ border: 0.5px solid rgba(6, 95, 70, 0.4);
1185
+ color: #a7f3d0;
1186
+ font-size: 11px;
1187
+ }
1188
+ .hqpu-alert-title {
1189
+ font-size: 11px;
1190
+ font-weight: 500;
1191
+ color: #fcd34d;
1192
+ }
1193
+ .hqpu-alert-body {
1194
+ margin: 2px 0 0;
1195
+ font-size: 10px;
1196
+ line-height: 15px;
1197
+ color: rgba(252, 211, 77, 0.8);
1198
+ }
1199
+ .hqpu-alert-body code {
1200
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1201
+ background: rgba(120, 53, 15, 0.5);
1202
+ padding: 1px 4px;
1203
+ border-radius: 3px;
1204
+ }
1205
+ .hqpu-alert-actions {
1206
+ display: flex;
1207
+ gap: 8px;
1208
+ margin-top: 6px;
1209
+ }
1210
+ .hqpu-alert-dismiss {
1211
+ background: none;
1212
+ border: 0;
1213
+ padding: 0;
1214
+ font-size: 10px;
1215
+ color: #fbbf24;
1216
+ cursor: pointer;
1217
+ text-decoration: underline;
1218
+ }
1219
+
1220
+ /* ── Runtime engine card ────────────────────────────────────────────────── */
1221
+ .hqpu-engine {
1222
+ padding: 12px;
1223
+ border-radius: 8px;
1224
+ border: 0.5px solid var(--hq-border);
1225
+ background: var(--hq-card);
1226
+ }
1227
+ .hqpu-engine-head {
1228
+ display: flex;
1229
+ align-items: center;
1230
+ justify-content: space-between;
1231
+ gap: 8px;
1232
+ }
1233
+ .hqpu-engine-title {
1234
+ display: flex;
1235
+ align-items: center;
1236
+ gap: 6px;
1237
+ font-size: 12px;
1238
+ font-weight: 600;
1239
+ color: #e4e4e7;
1240
+ }
1241
+ .hqpu-engine-title svg { width: 14px; height: 14px; color: #a1a1aa; }
1242
+ .hqpu-chip {
1243
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1244
+ font-size: 9px;
1245
+ padding: 2px 6px;
1246
+ border-radius: 4px;
1247
+ background: #27272a;
1248
+ border: 0.5px solid #52525b;
1249
+ color: #d4d4d8;
1250
+ }
1251
+ .hqpu-engine-update-row {
1252
+ display: flex;
1253
+ align-items: center;
1254
+ justify-content: space-between;
1255
+ gap: 8px;
1256
+ margin-top: 10px;
1257
+ padding: 8px;
1258
+ border-radius: 6px;
1259
+ border: 0.5px solid rgba(63, 63, 70, 0.7);
1260
+ background: rgba(24, 24, 27, 0.6);
1261
+ }
1262
+ .hqpu-engine-new-line {
1263
+ display: flex;
1264
+ align-items: center;
1265
+ gap: 4px;
1266
+ font-size: 11px;
1267
+ font-weight: 500;
1268
+ color: #d4d4d8;
1269
+ }
1270
+ .hqpu-tag--brand {
1271
+ font-size: 9px;
1272
+ padding: 1px 4px;
1273
+ border-radius: 3px;
1274
+ color: var(--hq-brand-soft);
1275
+ background: rgba(69, 10, 10, 0.6);
1276
+ border: 0.5px solid rgba(153, 27, 27, 0.5);
1277
+ }
1278
+ .hqpu-engine-size {
1279
+ margin: 3px 0 0;
1280
+ font-size: 10px;
1281
+ color: var(--hq-muted);
1282
+ overflow: hidden;
1283
+ text-overflow: ellipsis;
1284
+ white-space: nowrap;
1285
+ }
1286
+ .hqpu-btn--engine {
1287
+ flex: none;
1288
+ height: auto;
1289
+ padding: 6px 10px;
1290
+ font-size: 10px;
1291
+ background: #27272a;
1292
+ border: 0.5px solid #52525b;
1293
+ color: #e4e4e7;
1294
+ border-radius: 5px;
1295
+ }
1296
+ .hqpu-btn--engine:hover:not(:disabled) { background: #3f3f46; }
1297
+ .hqpu-engine-note {
1298
+ display: flex;
1299
+ align-items: center;
1300
+ gap: 4px;
1301
+ margin: 8px 0 0;
1302
+ font-size: 9px;
1303
+ color: var(--hq-muted);
1304
+ }
1305
+ .hqpu-engine-note svg { width: 10px; height: 10px; color: var(--hq-amber); flex: none; }
1306
+
1307
+ /* ── Plugin cards ───────────────────────────────────────────────────────── */
1308
+ .hqpu-section-head {
1309
+ display: flex;
1310
+ align-items: center;
1311
+ justify-content: space-between;
1312
+ gap: 8px;
1313
+ padding: 0 2px;
1314
+ }
1315
+ .hqpu-section-title {
1316
+ display: flex;
1317
+ align-items: center;
1318
+ gap: 6px;
1319
+ font-size: 12px;
1320
+ font-weight: 600;
1321
+ color: #e4e4e7;
1322
+ }
1323
+ .hqpu-count-pill {
1324
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1325
+ font-size: 10px;
1326
+ font-weight: 700;
1327
+ padding: 1px 6px;
1328
+ border-radius: 999px;
1329
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
1330
+ color: var(--hq-brand-soft);
1331
+ }
1332
+ .hqpu-all-changelog {
1333
+ background: none;
1334
+ border: 0;
1335
+ padding: 0;
1336
+ font-size: 10px;
1337
+ color: var(--hq-muted);
1338
+ cursor: pointer;
1339
+ }
1340
+ .hqpu-all-changelog:hover { color: var(--hq-text); }
1341
+
1342
+ .hqpu-cards { gap: 8px; }
1343
+ .hqpu-card {
1344
+ margin: 0;
1345
+ padding: 10px;
1346
+ border-radius: 8px;
1347
+ border: 0.5px solid var(--hq-border);
1348
+ background: var(--hq-card);
1349
+ transition: border-color 0.15s ease;
1350
+ }
1351
+ .hqpu-card:hover { border-color: #52525b; }
1352
+ .hqpu-card-head {
1353
+ display: flex;
1354
+ align-items: flex-start;
1355
+ justify-content: space-between;
1356
+ gap: 8px;
1357
+ padding: 0;
1358
+ }
1359
+ .hqpu-card-head > * { display: flex; align-items: center; gap: 8px; min-width: 0; }
1360
+ .hqpu-avatar {
1361
+ display: flex;
1362
+ align-items: center;
1363
+ justify-content: center;
1364
+ flex: none;
1365
+ width: 28px;
1366
+ height: 28px;
1367
+ border-radius: 6px;
1368
+ border: 0.5px solid #52525b;
1369
+ background: #27272a;
1370
+ color: #d4d4d8;
1371
+ font-size: 10px;
1372
+ font-weight: 700;
1373
+ }
1374
+ .hqpu-card-title {
1375
+ font-size: 12px;
1376
+ font-weight: 600;
1377
+ color: #e4e4e7;
1378
+ }
1379
+ .hqpu-card-sub {
1380
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1381
+ font-size: 9px;
1382
+ color: var(--hq-faint);
1383
+ }
1384
+ .hqpu-version-row {
1385
+ display: flex;
1386
+ align-items: center;
1387
+ gap: 4px;
1388
+ margin-top: 2px;
1389
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1390
+ font-size: 10px;
1391
+ }
1392
+ .hqpu-ver-old { color: var(--hq-muted); }
1393
+ .hqpu-ver-arrow { color: #52525b; }
1394
+ .hqpu-ver-new { color: var(--hq-emerald); font-weight: 700; }
1395
+ .hqpu-log-toggle {
1396
+ display: inline-flex;
1397
+ align-items: center;
1398
+ gap: 2px;
1399
+ background: none;
1400
+ border: 0;
1401
+ padding: 4px;
1402
+ font-size: 10px;
1403
+ color: var(--hq-muted);
1404
+ cursor: pointer;
1405
+ }
1406
+ .hqpu-log-toggle:hover { color: var(--hq-text); }
1407
+ .hqpu-log-toggle svg { width: 12px; height: 12px; transition: transform 0.15s ease; }
1408
+ .hqpu-log-toggle[data-open='true'] svg { transform: rotate(180deg); }
1409
+ .hqpu-changelog-drawer {
1410
+ margin-top: 8px;
1411
+ padding: 8px;
1412
+ border-radius: 6px;
1413
+ border: 0.5px solid #27272a;
1414
+ background: rgba(24, 24, 27, 0.8);
1415
+ font-size: 10px;
1416
+ }
1417
+ .hqpu-changelog-drawer-title {
1418
+ font-size: 9px;
1419
+ font-weight: 500;
1420
+ text-transform: uppercase;
1421
+ letter-spacing: 0.05em;
1422
+ color: #a1a1aa;
1423
+ }
1424
+ .hqpu-changelog-drawer ul {
1425
+ margin: 4px 0 0;
1426
+ padding-left: 16px;
1427
+ color: #d4d4d8;
1428
+ line-height: 16px;
1429
+ }
1430
+
1431
+ /* Blocked / incompatible card: dimmed, rose badge, no install button */
1432
+ .hqpu-card--blocked {
1433
+ opacity: 0.8;
1434
+ background: rgba(39, 39, 42, 0.6);
1435
+ border-color: #27272a;
1436
+ }
1437
+ .hqpu-card--blocked .hqpu-avatar {
1438
+ background: rgba(39, 39, 42, 0.5);
1439
+ border-color: #27272a;
1440
+ color: #71717a;
1441
+ }
1442
+ .hqpu-card--blocked .hqpu-card-title { color: #a1a1aa; }
1443
+ .hqpu-blocked-badge {
1444
+ font-size: 9px;
1445
+ padding: 2px 6px;
1446
+ border-radius: 4px;
1447
+ color: #fb7185;
1448
+ background: rgba(76, 5, 25, 0.5);
1449
+ border: 0.5px solid rgba(136, 19, 55, 0.5);
1450
+ }
1451
+ .hqpu-reason-line {
1452
+ display: flex;
1453
+ align-items: flex-start;
1454
+ gap: 4px;
1455
+ margin: 8px 0 0;
1456
+ padding: 6px;
1457
+ border-radius: 5px;
1458
+ border: 0.5px solid rgba(136, 19, 55, 0.2);
1459
+ background: rgba(76, 5, 25, 0.2);
1460
+ font-size: 10px;
1461
+ color: rgba(251, 113, 133, 0.9);
1462
+ }
1463
+ .hqpu-reason-line svg { width: 12px; height: 12px; flex: none; margin-top: 1px; }
1464
+
1465
+ /* Sticky footer */
1466
+ .hqpu-footer {
1467
+ flex: none;
1468
+ display: flex;
1469
+ flex-direction: column;
1470
+ align-items: stretch;
1471
+ gap: 8px;
1472
+ padding: 10px 12px 12px;
1473
+ border-top: 0.5px solid var(--hq-border);
1474
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
1475
+ }
1476
+ .hqpu-foot-meta {
1477
+ display: flex;
1478
+ align-items: center;
1479
+ justify-content: space-between;
1480
+ gap: 8px;
1481
+ font-size: 10px;
1482
+ color: var(--hq-muted);
1483
+ }
1484
+ .hqpu-foot-set {
1485
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1486
+ }
1487
+ .hqpu-foot-actions {
1488
+ display: flex;
1489
+ gap: 8px;
1490
+ }
1491
+ .hqpu-btn--block {
1492
+ flex: 1;
1493
+ height: 32px;
1494
+ background: var(--hq-brand);
1495
+ color: #fff;
1496
+ font-size: 11px;
1497
+ font-weight: 500;
1498
+ border-radius: 6px;
1499
+ box-shadow: 0 4px 12px rgba(127, 29, 29, 0.4);
1500
+ }
1501
+ .hqpu-btn--block:hover:not(:disabled) { background: var(--hq-brand-soft); }
1502
+ .hqpu-btn--block:disabled { opacity: 0.5; box-shadow: none; }
1503
+ .hqpu-btn--ignore {
1504
+ height: 32px;
1505
+ padding: 0 10px;
1506
+ background: var(--hq-card);
1507
+ border: 0.5px solid var(--hq-border);
1508
+ color: var(--hq-muted);
1509
+ font-size: 10px;
1510
+ border-radius: 6px;
1511
+ }
1512
+ .hqpu-btn--ignore:hover:not(:disabled) { background: var(--hq-hover); color: var(--hq-text); }
1513
+ .hqpu-footnote {
1514
+ margin: 0;
1515
+ font-size: 9px;
1516
+ text-align: center;
1517
+ color: var(--hq-muted);
1518
+ }
1519
+
1520
+ /* Shared bits on the new palette */
1521
+ .hqpu-status { color: var(--hq-muted); font-size: 11px; }
1522
+ .hqpu-empty { color: var(--hq-muted); font-size: 11px; padding: 16px 0; }
1523
+ .hqpu-toast { background: var(--hq-card); border: 0.5px solid var(--hq-border); color: var(--hq-text); font-size: 12px; }
1524
+ .hqpu-skeleton-fill { background: #3f3f46; }
1525
+ .hqpu-job { background: var(--hq-card); border-color: var(--hq-border); }
1526
+ .hqpu-job-meta { color: var(--hq-muted); }
1527
+ .hqpu-release-notes { color: var(--hq-muted); font-size: 11px; }
1528
+ .hqpu-advanced > summary { color: var(--hq-muted); }
1529
+ .hqpu-recent-jobs li { color: var(--hq-muted); font-size: 10px; }
792
1530
  `
793
1531
 
794
1532
  const STYLE_TAG_ID = 'data-plugin-css="huaqiu-plugin-update"'
@@ -929,8 +1667,15 @@ export interface ClientContext {
929
1667
  class UpdateStore implements UpdateStoreLike {
930
1668
  private current: PluginUpdateReport | null = null
931
1669
  private installingFlag = false
1670
+ /** The durable job the server currently reports as active (resumed across
1671
+ * panel close and DSH restart); null when idle. */
1672
+ private active: UpdateJob | null = null
1673
+ private recent: UpdateJob[] = []
932
1674
  private readonly listeners = new Set<() => void>()
933
1675
  private refreshTimer: number | null = null
1676
+ private pollTimer: number | null = null
1677
+ private pollBackoffMs = 1_000
1678
+ private pollInFlight = false
934
1679
 
935
1680
  report(): PluginUpdateReport | null {
936
1681
  return this.current
@@ -940,6 +1685,14 @@ class UpdateStore implements UpdateStoreLike {
940
1685
  return this.installingFlag
941
1686
  }
942
1687
 
1688
+ activeJob(): UpdateJob | null {
1689
+ return this.active
1690
+ }
1691
+
1692
+ recentJobs(): UpdateJob[] {
1693
+ return [...this.recent]
1694
+ }
1695
+
943
1696
  subscribe(listener: () => void): () => void {
944
1697
  this.listeners.add(listener)
945
1698
  return () => {
@@ -947,15 +1700,28 @@ class UpdateStore implements UpdateStoreLike {
947
1700
  }
948
1701
  }
949
1702
 
950
- /** Badge count per spec §6.3: actionable `update` + `install`, excluding the
951
- * ignored offer (the server already folds ignore into item states; keep the
952
- * client-side filter as a defensive second pass). */
1703
+ /**
1704
+ * Badge: counts actionable plugin updates AND an available runtime update AND
1705
+ * (while a job runs) an attention pill. A recent failed/rolled-back job with
1706
+ * nothing actionable left also keeps a dot, so the outcome is never silently
1707
+ * forgotten after the panel closes.
1708
+ */
953
1709
  badgeCount(): number {
1710
+ const job = this.active
1711
+ if (job !== null && !TERMINAL_PHASES.has(job.phase)) return 1
954
1712
  const report = this.current
955
1713
  if (report === null) return 0
956
- return report.items.filter(
1714
+ let count = report.items.filter(
957
1715
  (item) => item.state === 'update' || item.state === 'install',
958
1716
  ).length
1717
+ if (report.runtime?.updateAvailable) count += 1
1718
+ if (count === 0) {
1719
+ const latest = this.recent[0]
1720
+ if (latest !== undefined && (latest.phase === 'failed' || latest.phase === 'rolled-back')) {
1721
+ count = 1
1722
+ }
1723
+ }
1724
+ return count
959
1725
  }
960
1726
 
961
1727
  private emit(): void {
@@ -968,6 +1734,71 @@ class UpdateStore implements UpdateStoreLike {
968
1734
  }
969
1735
  }
970
1736
 
1737
+ /** Adopt a fresh `/status` snapshot and (re)arm the poll loop. */
1738
+ private applyStatus(status: UpdateJobStatus): void {
1739
+ const wasActive = this.active !== null
1740
+ this.active = status.activeJob
1741
+ this.recent = status.recentJobs
1742
+ if (wasActive && this.active === null) {
1743
+ // The active job settled (moved to recent/terminal) — the installing
1744
+ // footer state must clear on its own, without the panel's closure.
1745
+ this.installingFlag = false
1746
+ }
1747
+ this.emit()
1748
+ this.schedulePoll()
1749
+ }
1750
+
1751
+ /**
1752
+ * Fetch the durable job status. Called on mount, on panel open (BEFORE
1753
+ * discovery) and by the poll loop. A failure (older hq-edge with no
1754
+ * `/status`, or a transient HTTP error) is swallowed: the last known job is
1755
+ * kept and the poller backs off and retries.
1756
+ */
1757
+ async fetchStatus(): Promise<void> {
1758
+ try {
1759
+ const status = await fetchJobStatus()
1760
+ this.pollBackoffMs = 1_000
1761
+ this.applyStatus(status)
1762
+ } catch {
1763
+ this.schedulePoll()
1764
+ }
1765
+ }
1766
+
1767
+ /** ~1 s poll while a job is active; stop on terminal; back off on error
1768
+ * without discarding the last known job; never overlap requests. */
1769
+ private schedulePoll(): void {
1770
+ if (this.pollTimer !== null) {
1771
+ window.clearTimeout(this.pollTimer)
1772
+ this.pollTimer = null
1773
+ }
1774
+ const job = this.active
1775
+ if (job === null || TERMINAL_PHASES.has(job.phase)) {
1776
+ this.pollBackoffMs = 1_000
1777
+ return
1778
+ }
1779
+ this.pollTimer = window.setTimeout(() => {
1780
+ void this.pollTick()
1781
+ }, this.pollBackoffMs)
1782
+ }
1783
+
1784
+ private async pollTick(): Promise<void> {
1785
+ if (this.pollInFlight) {
1786
+ this.schedulePoll()
1787
+ return
1788
+ }
1789
+ this.pollInFlight = true
1790
+ try {
1791
+ const status = await fetchJobStatus()
1792
+ this.pollBackoffMs = 1_000
1793
+ this.applyStatus(status)
1794
+ } catch {
1795
+ this.pollBackoffMs = Math.min(5_000, Math.max(1_000, this.pollBackoffMs * 2))
1796
+ this.schedulePoll()
1797
+ } finally {
1798
+ this.pollInFlight = false
1799
+ }
1800
+ }
1801
+
971
1802
  async refresh(): Promise<PluginUpdateReport | null> {
972
1803
  try {
973
1804
  const report = await fetchReport()
@@ -1009,6 +1840,10 @@ class UpdateStore implements UpdateStoreLike {
1009
1840
  window.clearInterval(this.refreshTimer)
1010
1841
  this.refreshTimer = null
1011
1842
  }
1843
+ if (this.pollTimer !== null) {
1844
+ window.clearTimeout(this.pollTimer)
1845
+ this.pollTimer = null
1846
+ }
1012
1847
  this.listeners.clear()
1013
1848
  }
1014
1849
  }
@@ -1060,10 +1895,31 @@ export function apply(ctx: ClientContext): () => void {
1060
1895
  // publishes them after this plugin mounted must win, and one that never
1061
1896
  // does must keep the canonical fallbacks.
1062
1897
  ensureDesignTokens()
1063
- // Always re-check on open so the review screen never shows stale data.
1064
- void store.check().then((report) => {
1065
- if (report !== null) openUpdateView(store, copyFor())
1066
- })
1898
+ // Resume-before-discovery: read the durable job status FIRST so an in-flight
1899
+ // install/runtime update is resumed (and its view rendered) even while the
1900
+ // update check round trip is still running. The panel itself opens from
1901
+ // whatever the store already holds; check() refreshes the list behind it.
1902
+ void store.fetchStatus()
1903
+ openUpdateView(store, copyFor())
1904
+ void store.check()
1905
+ }
1906
+
1907
+ // While a job runs the row label shows its progress ("更新中 42%"), and the
1908
+ // tooltip names the phase. Store changes re-apply the label (the badge
1909
+ // subscription already re-renders the count pill).
1910
+ const statusLabel = (): { label: string; tooltip: string } => {
1911
+ const base = entryCopy(locale)
1912
+ const job = store.activeJob()
1913
+ if (job !== null) {
1914
+ const cc = copyFor()
1915
+ const pct = job.progress.percent
1916
+ const phase = cc.job.phase[job.phase] ?? job.phase
1917
+ return {
1918
+ label: pct !== null ? `${base.label} · ${Math.round(pct)}%` : `${base.label} · ${phase}`,
1919
+ tooltip: `${base.tooltip} · ${phase}`,
1920
+ }
1921
+ }
1922
+ return base
1067
1923
  }
1068
1924
 
1069
1925
  const disposeEntry = mountSidebarEntry({
@@ -1076,8 +1932,8 @@ export function apply(ctx: ClientContext): () => void {
1076
1932
  // `refresh.subscribe`'s listener, which re-applies these) so the row
1077
1933
  // re-localizes when the host UI language changes — matching the sibling
1078
1934
  // family rows, which read a live `currentLang` variable the same way.
1079
- label: () => entryCopy(locale).label,
1080
- tooltip: () => entryCopy(locale).tooltip,
1935
+ label: () => statusLabel().label,
1936
+ tooltip: () => statusLabel().tooltip,
1081
1937
  refresh: { subscribe: refreshSubscribe },
1082
1938
  badge: {
1083
1939
  subscribe: (listener) => store.subscribe(listener),
@@ -1088,8 +1944,21 @@ export function apply(ctx: ClientContext): () => void {
1088
1944
  familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,
1089
1945
  })
1090
1946
 
1947
+ // Store changes (active-job percentage, result dot) also re-apply the row
1948
+ // label, not just the badge count.
1949
+ store.subscribe(() => {
1950
+ for (const fn of [...localeSubscribers]) {
1951
+ try {
1952
+ fn()
1953
+ } catch {
1954
+ /* one bad subscriber must not strand the rest */
1955
+ }
1956
+ }
1957
+ })
1958
+
1091
1959
  store.startIdleRefresh()
1092
1960
  void store.refresh()
1961
+ void store.fetchStatus()
1093
1962
 
1094
1963
  const dispose = (): void => {
1095
1964
  try {