dsh-plugin-admin 0.2.0 → 0.3.0

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.
Files changed (2) hide show
  1. package/lib/client.js +284 -96
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -21,89 +21,106 @@ var useEffect = React.useEffect
21
21
  var CSS_TEXT = [
22
22
  '@keyframes dsh-admin-spin { to { transform: rotate(360deg); } }',
23
23
  '@keyframes dsh-admin-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.85); } }',
24
- '[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #0f1115); max-height: calc(100vh - 140px); overflow: hidden; }',
24
+ '[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); max-height: calc(100vh - 140px); overflow: hidden; }',
25
25
  '[data-dsh-admin-section] * { box-sizing: border-box; }',
26
- '[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.04)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08)); gap: 3px; }',
26
+ '[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
27
27
  '[data-dsh-admin-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
28
- '[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #111); background: rgba(0,0,0,0.02); }',
29
- '[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-base, #fff); color: var(--dsw-alias-label-primary, #111); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
30
- '[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, #e5e7eb); color: var(--dsw-alias-label-secondary, #555); }',
31
- '[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-alias-button-primary-fill, #1677ff); color: var(--dsw-alias-label-primary-foreground, #fff); }',
32
- '[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08)); border-radius: 12px; background: var(--dsw-alias-bg-base, #fff); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }',
33
- '[data-dsh-admin-section] .input-wrap { flex: 1; min-width: 0; position: relative; display: flex; align-items: center; }',
34
- '[data-dsh-admin-section] .input { width: 100%; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.12)); background: var(--dsw-alias-bg-base, #fff); color: inherit; font: inherit; outline: none; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }',
35
- '[data-dsh-admin-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.28)); }',
36
- '[data-dsh-admin-section] .input:focus { border-color: var(--dsw-alias-button-primary-fill, #1677ff); box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.14); }',
28
+ '[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
29
+ '[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
30
+ '[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
31
+ '[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
32
+ '[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }',
33
+ '[data-dsh-admin-section] .toolbar .input { height: 32px; padding: 0 12px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
34
+ '[data-dsh-admin-section] .search-wrap { flex: 1.6; min-width: 0; position: relative; display: flex; align-items: center; height: 32px; }',
35
+ '[data-dsh-admin-section] .search-wrap .input { flex: 1; min-width: 0; padding-left: 30px; font-size: 13px; }',
36
+ '[data-dsh-admin-section] .session-search { flex: 1; height: 32px; }',
37
+ '[data-dsh-admin-section] .session-search .input { height: 32px; padding: 0 12px; font-size: 13px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
38
+ '[data-dsh-admin-section] .install-wrap { flex: 1; min-width: 0; display: flex; align-items: center; height: 32px; }',
39
+ '[data-dsh-admin-section] .install-wrap .input { flex: 1; min-width: 0; font-size: 13px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
40
+ '[data-dsh-admin-section] .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 12px; opacity: 0.7; pointer-events: none; z-index: 1; }',
41
+ '[data-dsh-admin-section] .input { width: 100%; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); color: inherit; font: inherit; outline: none; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }',
42
+ '[data-dsh-admin-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); }',
43
+ '[data-dsh-admin-section] .input:focus { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
37
44
  '[data-dsh-admin-section] .filter-bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }',
38
- '[data-dsh-admin-section] .search-bar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08)); border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.02)); flex: none; }',
39
- '[data-dsh-admin-section] .search-icon { font-size: 12px; opacity: 0.7; flex: none; }',
40
- '[data-dsh-admin-section] .search-bar .input { padding: 6px 10px; }',
41
- '[data-dsh-admin-section] .pill { font-size: 11px; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.1)); background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary, #666); transition: all 0.12s; font-family: inherit; }',
42
- '[data-dsh-admin-section] .pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.04)); }',
43
- '[data-dsh-admin-section] .pill.active { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06)); color: var(--dsw-alias-button-primary-fill, #1677ff); font-weight: 600; border-color: currentColor; }',
44
- '[data-dsh-admin-section] .btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 30px; padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; cursor: pointer; font-family: inherit; transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.12)); background: transparent; color: inherit; white-space: nowrap; flex: none; }',
45
- '[data-dsh-admin-section] .btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.04)); border-color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.3)); }',
45
+ '[data-dsh-admin-section] .pill { font-size: 11px; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.45)); background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary, #555); transition: all 0.12s; font-family: inherit; }',
46
+ '[data-dsh-admin-section] .pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
47
+ '[data-dsh-admin-section] .pill.active { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 600; border-color: currentColor; }',
48
+ '[data-dsh-admin-section] .btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; height: 32px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-interactive-bg-hover, rgba(240,240,245,0.6)); color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; flex: none; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }',
49
+ '[data-dsh-admin-section] .btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(220,220,230,0.7)); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.65)); color: var(--dsw-alias-label-primary, #222); box-shadow: 0 2px 4px rgba(0,0,0,0.06); }',
50
+ '[data-dsh-admin-section] .search-wrap .btn.sm { background: transparent; border: none; box-shadow: none; padding: 2px 6px; color: var(--dsw-alias-label-tertiary, #888); }',
51
+ '[data-dsh-admin-section] .search-wrap .btn.sm:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-primary, #222); }',
46
52
  '[data-dsh-admin-section] .btn:active:not(:disabled) { transform: translateY(1px); }',
47
- '[data-dsh-admin-section] .btn:focus-visible, [data-dsh-admin-section] .pill:focus-visible { outline: 2px solid var(--dsw-alias-button-primary-fill, #1677ff); outline-offset: 2px; }',
53
+ '[data-dsh-admin-section] .btn:focus-visible, [data-dsh-admin-section] .pill:focus-visible { outline: 2px solid var(--dsw-static-blue-500, #3b82f6); outline-offset: 2px; }',
48
54
  '[data-dsh-admin-section] .btn:disabled { opacity: 0.45; cursor: not-allowed; }',
49
- '[data-dsh-admin-section] .btn.primary { background: var(--dsw-alias-button-primary-fill, #1677ff); color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; font-weight: 600; }',
50
- '[data-dsh-admin-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, #4096ff); }',
51
- '[data-dsh-admin-section] .btn.danger { color: var(--dsw-alias-state-error-primary, #ef4444); border-color: var(--dsw-alias-border-subtle, rgba(0,0,0,0.12)); }',
52
- '[data-dsh-admin-section] .btn.danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.1)); }',
53
- '[data-dsh-admin-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #ef4444); color: #fff; border-color: transparent; }',
54
- '[data-dsh-admin-section] .btn.danger-solid:hover:not(:disabled) { opacity: 0.9; }',
55
- '[data-dsh-admin-section] .btn.sm { padding: 3px 8px; font-size: 11px; border-radius: 6px; }',
55
+ '[data-dsh-admin-section] .btn.primary { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; border-color: transparent; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }',
56
+ '[data-dsh-admin-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-static-blue-400, #60a5fa); color: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.14); }',
57
+ '[data-dsh-admin-section] .btn.primary:active:not(:disabled) { background: var(--dsw-static-blue-600, #2563eb); color: #fff; }',
58
+ '[data-dsh-admin-section] .btn.danger { color: var(--dsw-alias-state-error-primary, #dc2626); border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
59
+ '[data-dsh-admin-section] .btn.danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.1)); border-color: rgba(220,38,38,0.35); color: #b91c1c; }',
60
+ '[data-dsh-admin-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #dc2626); color: #fff; border-color: transparent; }',
61
+ '[data-dsh-admin-section] .btn.danger-solid:hover:not(:disabled) { background: #b91c1c; color: #fff; }',
62
+ '[data-dsh-admin-section] .btn.danger-solid:active:not(:disabled) { background: #991b1b; color: #fff; }',
63
+ '[data-dsh-admin-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
56
64
  '[data-dsh-admin-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-admin-spin 0.7s linear infinite; display: inline-block; flex: none; }',
57
- '[data-dsh-admin-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(0,0,0,0.2)) transparent; }',
65
+ '[data-dsh-admin-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
58
66
  '[data-dsh-admin-section] .list::-webkit-scrollbar { width: 6px; }',
59
- '[data-dsh-admin-section] .list::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-subtle, rgba(0,0,0,0.2)); }',
67
+ '[data-dsh-admin-section] .list::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
60
68
  '[data-dsh-admin-section] .session-group { display: flex; flex-direction: column; gap: 6px; }',
61
- '[data-dsh-admin-section] .group-header { display: flex; align-items: center; gap: 8px; padding: 10px 4px 4px 4px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); border-bottom: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08)); }',
62
- '[data-dsh-admin-section] .group-header:first-child { padding-top: 4px; }',
69
+ '[data-dsh-admin-section] .group-header { display: flex; align-items: center; gap: 10px; padding: 10px 12px 8px 12px; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); margin-bottom: 4px; }',
70
+ '[data-dsh-admin-section] .group-header:first-child { margin-top: 2px; }',
63
71
  '[data-dsh-admin-section] .group-title { display: inline-flex; align-items: center; gap: 6px; }',
64
- '[data-dsh-admin-section] .group-count { font-size: 10px; padding: 1px 7px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05)); color: var(--dsw-alias-label-secondary, #555); font-weight: 500; }',
72
+ '[data-dsh-admin-section] .group-count { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); color: var(--dsw-alias-label-secondary, #555); font-weight: 500; }',
73
+ '[data-dsh-admin-section] .group-header .btn.sm { margin-left: auto; }',
74
+ '[data-dsh-admin-section] .group-header .btn.sm + .btn.sm { margin-left: 0; }',
65
75
  '[data-dsh-admin-section] .group-path { font-family: monospace; font-size: 10px; color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; text-align: right; }',
66
- '[data-dsh-admin-section] .card { display: flex; flex-direction: column; gap: 7px; padding: 12px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08)); background: var(--dsw-alias-bg-base, #fff); box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }',
67
- '[data-dsh-admin-section] .card:hover { transform: translateY(-1px); border-color: var(--dsw-alias-label-tertiary, rgba(128,128,128,0.3)); box-shadow: 0 8px 20px rgba(0,0,0,0.06); }',
76
+ '[data-dsh-admin-section] .card { display: flex; flex-direction: column; gap: 7px; padding: 12px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }',
77
+ '[data-dsh-admin-section] .card:hover { transform: translateY(-1px); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); box-shadow: 0 8px 20px rgba(0,0,0,0.06); }',
68
78
  '[data-dsh-admin-section] .card-header { display: flex; align-items: center; gap: 8px; min-width: 0; }',
69
79
  '[data-dsh-admin-section] .card-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 7px; min-width: 0; flex: 1; }',
70
80
  '[data-dsh-admin-section] .card-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }',
71
- '[data-dsh-admin-section] .card-summary { display: flex; align-items: flex-start; gap: 6px; padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.025)); border-left: 2px solid var(--dsw-alias-button-primary-fill, #1677ff); font-size: 12px; line-height: 1.45; color: var(--dsw-alias-label-secondary, #555); margin-top: 1px; margin-bottom: 1px; }',
81
+ '[data-dsh-admin-section] .card-summary { display: flex; align-items: flex-start; gap: 6px; padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); border-left: 2px solid var(--dsw-static-blue-500, #3b82f6); font-size: 12px; line-height: 1.45; color: var(--dsw-alias-label-secondary, #555); margin-top: 1px; margin-bottom: 1px; }',
72
82
  '[data-dsh-admin-section] .summary-icon { font-size: 11px; line-height: 1.45; flex: none; opacity: 0.85; }',
73
83
  '[data-dsh-admin-section] .summary-text { flex: 1; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; }',
74
84
  '[data-dsh-admin-section] .card-sub { font-size: 11px; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
75
85
  '[data-dsh-admin-section] .card-sub-item { display: inline-flex; align-items: center; gap: 3px; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
76
86
  '[data-dsh-admin-section] .card-actions { display: flex; gap: 5px; align-items: center; flex: none; }',
77
- '[data-dsh-admin-section] .tag { font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.04)); color: var(--dsw-alias-label-secondary, #666); }',
87
+ '[data-dsh-admin-section] .tag { font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); color: var(--dsw-alias-label-secondary, #555); }',
78
88
  '[data-dsh-admin-section] .tag.plugin { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); }',
79
- '[data-dsh-admin-section] .tag.local { background: rgba(59, 130, 246, 0.1); color: var(--dsw-alias-button-primary-fill, #1677ff); }',
89
+ '[data-dsh-admin-section] .tag.local { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); }',
80
90
  '[data-dsh-admin-section] .tag.update { background: rgba(245, 158, 11, 0.14); color: #d97706; font-weight: 600; border: 1px solid rgba(217, 119, 6, 0.25); }',
81
91
  '[data-dsh-admin-section] .tag.update-error { background: rgba(239, 68, 68, 0.1); color: var(--dsw-alias-state-error-primary, #ef4444); }',
82
- '[data-dsh-admin-section] .plugin-path { font-family: monospace; font-size: 11px; line-height: 1.4; color: var(--dsw-alias-label-secondary, #666); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 4px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.025)); }',
92
+ '[data-dsh-admin-section] .plugin-path { font-family: monospace; font-size: 11px; line-height: 1.4; color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 4px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); }',
83
93
  '[data-dsh-admin-section] .tag.version { font-family: monospace; }',
84
94
  '[data-dsh-admin-section] .tag.live { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); font-weight: 600; }',
85
95
  '[data-dsh-admin-section] .tag.archived { background: rgba(245, 158, 11, 0.12); color: var(--dsw-alias-state-warn-label, #f59e0b); }',
86
- '[data-dsh-admin-section] .tag.turns { background: rgba(59, 130, 246, 0.1); color: var(--dsw-alias-button-primary-fill, #1677ff); font-weight: 500; }',
96
+ '[data-dsh-admin-section] .tag.turns { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 500; }',
87
97
  '[data-dsh-admin-section] .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dsw-alias-label-tertiary, #999); }',
88
98
  '[data-dsh-admin-section] .dot.live { background: var(--dsw-alias-state-success-primary, #10b981); box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25); animation: dsh-admin-pulse 2s infinite ease-in-out; }',
89
99
  '[data-dsh-admin-section] .dot.archived { background: var(--dsw-alias-state-warn-label, #f59e0b); }',
90
100
  '[data-dsh-admin-section] .confirm-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.08)); border: 1px solid rgba(239,68,68,0.2); }',
91
101
  '[data-dsh-admin-section] .confirm-text { font-size: 11px; color: var(--dsw-alias-state-error-primary, #ef4444); font-weight: 500; }',
92
102
  '[data-dsh-admin-section] .confirm-actions { display: flex; gap: 5px; flex: none; }',
93
- '[data-dsh-admin-section] .busy-banner { display: flex; align-items: center; gap: 8px; padding: 7px 11px; border-radius: 7px; background: rgba(22, 119, 255, 0.08); color: var(--dsw-alias-button-primary-fill, #1677ff); font-size: 11px; }',
103
+ '[data-dsh-admin-section] .busy-banner { display: flex; align-items: center; gap: 8px; padding: 7px 11px; border-radius: 7px; background: rgba(59, 130, 246, 0.08); color: var(--dsw-static-blue-500, #3b82f6); font-size: 11px; }',
104
+ '[data-dsh-admin-section] .update-strip { display: flex; align-items: center; gap: 8px; padding: 8px 11px; border-radius: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; }',
105
+ '[data-dsh-admin-section] .update-strip .btn.sm { margin-left: auto; flex: none; }',
106
+ '[data-dsh-admin-section] .update-strip.checking { background: rgba(59, 130, 246, 0.08); color: var(--dsw-static-blue-500, #3b82f6); }',
107
+ '[data-dsh-admin-section] .update-strip.ok { background: rgba(16, 185, 129, 0.1); color: var(--dsw-alias-state-success-primary, #10b981); }',
108
+ '[data-dsh-admin-section] .update-strip.has-updates { background: rgba(245, 158, 11, 0.12); color: #d97706; border: 1px solid rgba(217, 119, 6, 0.25); }',
109
+ '[data-dsh-admin-section] .update-strip.has-errors { background: rgba(239, 68, 68, 0.08); color: var(--dsw-alias-state-error-primary, #ef4444); }',
94
110
  '[data-dsh-admin-section] .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px 12px; color: var(--dsw-alias-label-tertiary, #999); gap: 4px; font-size: 12px; text-align: center; }',
95
111
  '[data-dsh-admin-section] .error { padding: 7px 11px; border-radius: 7px; color: var(--dsw-alias-state-error-primary, #ef4444); background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.08)); font-size: 12px; white-space: pre-wrap; max-height: 100px; overflow-y: auto; }',
96
- '[data-dsh-admin-section] .footer { display: flex; justify-content: space-between; align-items: center; padding-top: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); border-top: 1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.06)); flex: none; }',
112
+ '[data-dsh-admin-section] .footer { display: flex; justify-content: space-between; align-items: center; padding-top: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); border-top: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); flex: none; }',
97
113
  '[data-dsh-admin-section] .footer .path { max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
98
114
  '[data-dsh-admin-section] .footer .hint { font-style: normal; }',
99
115
  '[data-dsh-admin-section] .session-id-badge { font-family: monospace; font-size: 10px; opacity: 0.75; }',
100
- '[data-dsh-admin-section] .mcp-test-row { padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.025)); font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }',
116
+ '[data-dsh-admin-section] .mcp-test-row { padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }',
101
117
  '[data-dsh-admin-section] .mcp-test { display: inline-flex; align-items: center; gap: 5px; }',
102
118
  '[data-dsh-admin-section] .mcp-test.mcp-test-list { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }',
103
119
  '[data-dsh-admin-section] .mcp-test-busy { color: var(--dsw-alias-label-secondary, #666); }',
104
120
  '[data-dsh-admin-section] .mcp-test-ok { color: #16a34a; }',
105
121
  '[data-dsh-admin-section] .mcp-test-fail { color: var(--dsw-alias-state-error-primary, #ef4444); }',
106
122
  '[data-dsh-admin-section] .mcp-test-warn { color: #d97706; opacity: 0.95; }',
123
+ '[data-dsh-admin-section] .mcp-test-cached { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); }',
107
124
  '@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
108
125
  '@keyframes dsh-toast-in { 0% { opacity: 0; transform: translateY(-12px) scale(0.96); } 100% { opacity: 1; transform: translateY(0) scale(1); } }',
109
126
  '@keyframes dsh-toast-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-8px) scale(0.96); } }',
@@ -413,7 +430,7 @@ function setupMenuInjection(call, refreshSessions) {
413
430
  var sep = document.createElement('div')
414
431
  sep.setAttribute('role', 'separator')
415
432
  sep.setAttribute('data-dsh-admin-injected', '')
416
- sep.style.cssText = 'margin:3px 0;border-top:1px solid var(--dsw-alias-border-subtle,rgba(0,0,0,0.08))'
433
+ sep.style.cssText = 'margin:3px 0;border-top:1px solid var(--dsw-alias-border-subtle,rgba(200,200,210,0.3))'
417
434
  viewport.appendChild(sep)
418
435
 
419
436
  var btn = document.createElement('button')
@@ -422,7 +439,7 @@ function setupMenuInjection(call, refreshSessions) {
422
439
  btn.setAttribute('data-dsh-admin-injected', '')
423
440
  btn.textContent = label
424
441
  var color = kind === 'danger' ? '#ef4444' : 'inherit'
425
- var hoverBg = kind === 'danger' ? 'rgba(239,68,68,0.1)' : 'rgba(0,0,0,0.06)'
442
+ var hoverBg = kind === 'danger' ? 'rgba(239,68,68,0.12)' : 'rgba(200,200,210,0.3)'
426
443
  btn.style.cssText = 'display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;background:transparent;cursor:pointer;font:inherit;font-size:12px;color:' + color + ';border-radius:5px;text-align:left'
427
444
  btn.addEventListener('mouseenter', function () { btn.style.background = hoverBg })
428
445
  btn.addEventListener('mouseleave', function () { btn.style.background = 'transparent' })
@@ -492,6 +509,7 @@ function PluginsSection(props) {
492
509
  filter: 'all',
493
510
  needle: '',
494
511
  checkingUpdates: false,
512
+ updateChecked: false, // true once a check finished (even if no updates)
495
513
  updates: {}, // name -> { latest, updateAvailable, error? }
496
514
  })
497
515
  var pView = pairPlugins[0]
@@ -549,17 +567,24 @@ function PluginsSection(props) {
549
567
  var list = (result.value && result.value.updates) || []
550
568
  var map = {}
551
569
  var updateCount = 0
570
+ var checkedCount = 0
571
+ var checkErrorCount = 0
552
572
  for (var i = 0; i < list.length; i++) {
553
573
  var u = list[i]
554
574
  map[u.name] = { latest: u.latest, updateAvailable: u.updateAvailable === true, error: u.error || '' }
555
575
  if (u.updateAvailable === true) updateCount++
576
+ if (u.error) checkErrorCount++
577
+ checkedCount++
556
578
  }
557
579
  var updated = map
558
580
  patchPlugin({
559
581
  updates: updated,
582
+ updateChecked: true,
560
583
  note: updateCount > 0
561
584
  ? '发现 ' + updateCount + ' 个插件有新版本'
562
- : (list.length > 0 ? '已检查 ' + list.length + ' 个插件,均为最新版本' : ''),
585
+ : (checkErrorCount > 0
586
+ ? '已检查 ' + checkedCount + ' 个插件(' + checkErrorCount + ' 个查询失败),其余均为最新版本'
587
+ : (checkedCount > 0 ? '已检查 ' + checkedCount + ' 个插件,均为最新版本' : '')),
563
588
  })
564
589
  }, function (failure) {
565
590
  if (!alive.current) return
@@ -654,7 +679,7 @@ function PluginsSection(props) {
654
679
  }, [])
655
680
 
656
681
  return createElement('div', { 'data-dsh-admin-section': '' },
657
- renderPluginsView(pView, patchPlugin, reloadPlugins, installPlugin, removePlugin, checkUpdates, upgradePlugin))
682
+ renderPluginsView(pView, patchPlugin, installPlugin, removePlugin, checkUpdates, upgradePlugin))
658
683
  }
659
684
 
660
685
  /**
@@ -738,6 +763,101 @@ function SessionsSection(props) {
738
763
  /**
739
764
  * MCP configuration settings section (standalone, no internal tabs).
740
765
  */
766
+
767
+ /* ------------------------------------------------------------------------ */
768
+ /* MCP test-result cache */
769
+ /* */
770
+ /* Successful connectivity probes are persisted to localStorage so the */
771
+ /* panel shows the last known status after tab switches or reopening the */
772
+ /* dialog instead of an empty state. Only OK results are cached (failures */
773
+ /* are usually transient); a cached row is labelled with its probe time, */
774
+ /* and the cache entry is invalidated when the config is saved or removed, */
775
+ /* or pruned when the entry disappears from the host list. */
776
+ /* ------------------------------------------------------------------------ */
777
+
778
+ var MCP_TEST_CACHE_KEY = 'dsh-plugin-admin/mcp-test-results'
779
+
780
+ /**
781
+ * Touch localStorage safely: on some environments (jsdom with an opaque
782
+ * origin, hardened browsers, privacy modes) merely READING window.localStorage
783
+ * throws a SecurityError, so the access itself has to sit inside try/catch.
784
+ * Returns null when storage is unavailable.
785
+ */
786
+ function mcpTestStorage() {
787
+ try {
788
+ if (typeof window === 'undefined' || !window.localStorage) return null
789
+ return window.localStorage
790
+ } catch (e) {
791
+ return null
792
+ }
793
+ }
794
+
795
+ function loadMcpTestCache() {
796
+ var store = mcpTestStorage()
797
+ if (store === null) return {}
798
+ try {
799
+ var parsed = JSON.parse(store.getItem(MCP_TEST_CACHE_KEY))
800
+ return parsed !== null && typeof parsed === 'object' ? parsed : {}
801
+ } catch (e) {
802
+ return {}
803
+ }
804
+ }
805
+
806
+ /** Mirror settled, successful probes into localStorage (best-effort). */
807
+ function saveMcpTestCache(state) {
808
+ var store = mcpTestStorage()
809
+ if (store === null) return
810
+ try {
811
+ var out = {}
812
+ for (var id in state) {
813
+ var s = state[id]
814
+ // Busy rows keep the previous cached result; failures stay ephemeral.
815
+ if (!s || s.busy || s.result === null || s.result === undefined) continue
816
+ out[id] = { result: s.result, at: typeof s.at === 'number' ? s.at : Date.now() }
817
+ }
818
+ store.setItem(MCP_TEST_CACHE_KEY, JSON.stringify(out))
819
+ } catch (e) {
820
+ // Quota exceeded / storage blocked — the cache is optional, never fatal.
821
+ }
822
+ }
823
+
824
+ /** Drop one entry's cached probe (config saved or entry removed). */
825
+ function clearMcpTestCacheEntry(id) {
826
+ var store = mcpTestStorage()
827
+ if (store === null) return
828
+ try {
829
+ var state = loadMcpTestCache()
830
+ if (state[id] === undefined) return
831
+ delete state[id]
832
+ store.setItem(MCP_TEST_CACHE_KEY, JSON.stringify(state))
833
+ } catch (e) { /* ignore */ }
834
+ }
835
+
836
+ /** Drop test states whose entry no longer exists in the host list. */
837
+ function pruneMcpTestState(map, entries) {
838
+ var valid = {}
839
+ for (var i = 0; i < entries.length; i++) valid[entries[i].id] = true
840
+ var next = {}
841
+ var changed = false
842
+ for (var id in map) {
843
+ if (valid[id]) next[id] = map[id]
844
+ else changed = true
845
+ }
846
+ if (changed) saveMcpTestCache(next)
847
+ return next
848
+ }
849
+
850
+ /** Compact timestamp for a cached probe ("14:32" today, "6-1 14:32" older). */
851
+ function formatTestTime(ms) {
852
+ if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''
853
+ var d = new Date(ms)
854
+ var pad = function (n) { return (n < 10 ? '0' : '') + String(n) }
855
+ var hm = pad(d.getHours()) + ':' + pad(d.getMinutes())
856
+ var now = new Date()
857
+ if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) return hm
858
+ return (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hm
859
+ }
860
+
741
861
  function McpSection(props) {
742
862
  var pairMcp = useState({
743
863
  mcpEntries: [],
@@ -745,7 +865,9 @@ function McpSection(props) {
745
865
  mcpError: '',
746
866
  mcpEditorOpen: false,
747
867
  mcpDraft: null,
748
- mcpTestState: {}, // entryId -> { busy, result }
868
+ // entryId -> { busy?, result?, error?, at? }; previous successful probes
869
+ // are restored from localStorage so statuses survive panel remounts.
870
+ mcpTestState: loadMcpTestCache(),
749
871
  })
750
872
  var mView = pairMcp[0]
751
873
  var setMView = pairMcp[1]
@@ -781,7 +903,16 @@ function McpSection(props) {
781
903
  callRemote('mcpAdmin/list', {}).then(function (result) {
782
904
  if (!alive.current) return
783
905
  if (result.ok) {
784
- patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [] })
906
+ var entries = (result.value && result.value.entries) || []
907
+ setMView(function (cur) {
908
+ var next = {}
909
+ for (var k in cur) next[k] = cur[k]
910
+ next.mcpBusy = false
911
+ next.mcpEntries = entries
912
+ // Drop cached probes whose entry no longer exists on the host.
913
+ next.mcpTestState = pruneMcpTestState(cur.mcpTestState, entries)
914
+ return next
915
+ })
785
916
  } else {
786
917
  patchMcp({ mcpBusy: false, mcpError: '加载 MCP 配置失败:' + messageOf(result.error) })
787
918
  }
@@ -904,6 +1035,10 @@ function McpSection(props) {
904
1035
  callRemote('mcpAdmin/upsert', { entry: { id: draft.id, config: config } }).then(function (result) {
905
1036
  if (!alive.current) return
906
1037
  if (result.ok) {
1038
+ // The config changed, so any cached probe no longer describes this
1039
+ // server — drop it from storage and from the panel.
1040
+ clearMcpTestCacheEntry(draft.id)
1041
+ patchMcpTest(draft.id, { busy: false, result: null, error: null })
907
1042
  patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [], mcpEditorOpen: false, mcpDraft: null })
908
1043
  } else {
909
1044
  patchMcp({ mcpBusy: false, mcpError: '保存 MCP 配置失败:' + messageOf(result.error) })
@@ -919,6 +1054,8 @@ function McpSection(props) {
919
1054
  callRemote('mcpAdmin/remove', { id: id }).then(function (result) {
920
1055
  if (!alive.current) return
921
1056
  if (result.ok) {
1057
+ // The entry is gone — its cached probe must not linger in storage.
1058
+ clearMcpTestCacheEntry(id)
922
1059
  patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [] })
923
1060
  } else {
924
1061
  patchMcp({ mcpBusy: false, mcpError: '移除 MCP 配置失败:' + messageOf(result.error) })
@@ -929,12 +1066,18 @@ function McpSection(props) {
929
1066
  })
930
1067
  }
931
1068
 
932
- /** Set one entry's connectivity-test status against the CURRENT state. */
1069
+ /**
1070
+ * Set one entry's connectivity-test status against the CURRENT state.
1071
+ * Settled successful probes are mirrored into localStorage so the last
1072
+ * known status survives tab switches and reopening the settings dialog.
1073
+ */
933
1074
  function patchMcpTest(id, partial) {
934
1075
  setMView(function (cur) {
1076
+ var nextTestState = mergeTestState(cur.mcpTestState, id, partial)
1077
+ saveMcpTestCache(nextTestState)
935
1078
  var next = {}
936
1079
  for (var k in cur) next[k] = cur[k]
937
- next.mcpTestState = mergeTestState(cur.mcpTestState, id, partial)
1080
+ next.mcpTestState = nextTestState
938
1081
  return next
939
1082
  })
940
1083
  }
@@ -945,7 +1088,8 @@ function McpSection(props) {
945
1088
  callRemote('mcpAdmin/test', { id: id }).then(function (result) {
946
1089
  if (!alive.current) return
947
1090
  if (result.ok && result.value !== null && typeof result.value === 'object') {
948
- patchMcpTest(id, { busy: false, result: result.value })
1091
+ // `at` timestamps the probe; it is what the cached label renders.
1092
+ patchMcpTest(id, { busy: false, result: result.value, at: Date.now() })
949
1093
  } else {
950
1094
  patchMcpTest(id, { busy: false, result: null, error: messageOf(result.error) })
951
1095
  }
@@ -998,15 +1142,32 @@ function AdminCenterSection(props) {
998
1142
  /* Render Plugins View */
999
1143
  /* ========================================================================== */
1000
1144
 
1001
- function renderPluginsView(view, patch, reload, install, remove, checkUpdates, upgrade) {
1145
+ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade) {
1002
1146
  var elements = []
1003
1147
 
1004
- // Toolbar
1148
+ // Toolbar: search (primary, wide) + install input + actions in one row.
1005
1149
  elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
1006
- createElement('div', { className: 'input-wrap', key: 'wrap' },
1150
+ createElement('div', { className: 'search-wrap', key: 'search-wrap' },
1151
+ createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
1007
1152
  createElement('input', {
1008
1153
  className: 'input',
1009
- placeholder: '输入 npm 包名或本地绝对路径...',
1154
+ key: 'search-input',
1155
+ placeholder: '搜索插件(名称/版本/路径)...',
1156
+ value: view.needle,
1157
+ onChange: function (e) { patch({ needle: e.target.value }) },
1158
+ }),
1159
+ view.needle !== '' ? createElement('button', {
1160
+ type: 'button',
1161
+ key: 'btn-clear-search',
1162
+ className: 'btn sm',
1163
+ title: '清空搜索',
1164
+ onClick: function () { patch({ needle: '' }) },
1165
+ }, '✕') : null
1166
+ ),
1167
+ createElement('div', { className: 'input-wrap install-wrap', key: 'install-wrap' },
1168
+ createElement('input', {
1169
+ className: 'input',
1170
+ placeholder: '安装包名/路径...',
1010
1171
  value: view.spec,
1011
1172
  disabled: view.busy,
1012
1173
  onChange: function (e) { patch({ spec: e.target.value }) },
@@ -1031,34 +1192,8 @@ function renderPluginsView(view, patch, reload, install, remove, checkUpdates, u
1031
1192
  onClick: checkUpdates,
1032
1193
  },
1033
1194
  view.checkingUpdates ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
1034
- view.checkingUpdates ? '检查中...' : ' 检查更新'
1195
+ view.checkingUpdates ? '检查中...' : '⬆️ 检查更新'
1035
1196
  ),
1036
- createElement('button', {
1037
- type: 'button',
1038
- key: 'btn-refresh',
1039
- className: 'btn',
1040
- disabled: view.busy,
1041
- onClick: reload,
1042
- }, '刷新')
1043
- ))
1044
-
1045
- // Search bar: fuzzy filter by plugin name / version / local path.
1046
- elements.push(createElement('div', { className: 'search-bar', key: 'search' },
1047
- createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
1048
- createElement('input', {
1049
- className: 'input',
1050
- key: 'search-input',
1051
- placeholder: '按名称/版本/路径模糊搜索插件...',
1052
- value: view.needle,
1053
- onChange: function (e) { patch({ needle: e.target.value }) },
1054
- }),
1055
- view.needle !== '' ? createElement('button', {
1056
- type: 'button',
1057
- key: 'btn-clear-search',
1058
- className: 'btn sm',
1059
- title: '清空搜索',
1060
- onClick: function () { patch({ needle: '' }) },
1061
- }, '✕') : null
1062
1197
  ))
1063
1198
 
1064
1199
  // Filter Pills
@@ -1102,6 +1237,41 @@ function renderPluginsView(view, patch, reload, install, remove, checkUpdates, u
1102
1237
  elements.push(createElement('div', { className: 'error', key: 'error' }, view.error))
1103
1238
  }
1104
1239
 
1240
+ // Update-check status strip: gives the auto-check a visible outcome instead
1241
+ // of hiding it in the footer. Checking → spinner; done with updates →
1242
+ // orange; done without updates → green; per-plugin query failures surface
1243
+ // on the strip rather than vanishing.
1244
+ if (view.checkingUpdates) {
1245
+ elements.push(createElement('div', { className: 'update-strip checking', key: 'update-checking' },
1246
+ createElement('span', { className: 'spinner', key: 'spin' }),
1247
+ '正在检查插件版本更新…'
1248
+ ))
1249
+ } else if (view.updateChecked) {
1250
+ var upCount = 0
1251
+ var errCount = 0
1252
+ for (var un in view.updates) {
1253
+ if (view.updates[un].updateAvailable === true) upCount++
1254
+ if (view.updates[un].error) errCount++
1255
+ }
1256
+ var stripClass = upCount > 0 ? 'update-strip has-updates'
1257
+ : (errCount > 0 ? 'update-strip has-errors' : 'update-strip ok')
1258
+ var stripText = upCount > 0
1259
+ ? '⬆️ 发现 ' + upCount + ' 个插件有新版本,可点击卡片上的「更新」升级'
1260
+ : (errCount > 0
1261
+ ? '⚠️ 有 ' + errCount + ' 个插件查询版本失败(网络或 registry 不可达)'
1262
+ : '✅ 已自动检查版本更新,全部为最新版本')
1263
+ elements.push(createElement('div', { className: stripClass, key: 'update-result' },
1264
+ createElement('span', { key: 't' }, stripText),
1265
+ createElement('button', {
1266
+ type: 'button',
1267
+ key: 'btn-recheck',
1268
+ className: 'btn sm',
1269
+ disabled: view.busy || view.checkingUpdates,
1270
+ onClick: checkUpdates,
1271
+ }, '🔄 重新检查')
1272
+ ))
1273
+ }
1274
+
1105
1275
  // Filter and render rows
1106
1276
  var needle = view.needle.trim().toLowerCase()
1107
1277
  var filtered = filterPlugins(view.plugins, view.filter, needle)
@@ -1133,23 +1303,23 @@ function renderPluginsView(view, patch, reload, install, remove, checkUpdates, u
1133
1303
 
1134
1304
  function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry) {
1135
1305
  var children = []
1136
- var header = createElement('div', { className: 'group-header', key: 'mcp-header' },
1306
+ var header = createElement('div', { className: 'group-header', key: 'mcp-header', style: { marginTop: 0 } },
1137
1307
  createElement('span', { className: 'group-title', key: 't' }, '🔌 MCP 配置'),
1138
1308
  createElement('span', { className: 'group-count', key: 'c' }, String(view.mcpEntries.length) + ' 个'),
1139
1309
  createElement('button', {
1140
1310
  type: 'button',
1141
1311
  key: 'btn-refresh-mcp',
1142
- className: 'btn sm',
1312
+ className: 'btn',
1143
1313
  disabled: view.mcpBusy,
1144
1314
  onClick: reloadMcp,
1145
- }, '刷新'),
1315
+ }, '🔄 刷新'),
1146
1316
  createElement('button', {
1147
1317
  type: 'button',
1148
1318
  key: 'btn-add-mcp',
1149
- className: 'btn primary sm',
1319
+ className: 'btn primary',
1150
1320
  disabled: view.mcpBusy,
1151
1321
  onClick: function () { openMcpEditor(null) },
1152
- }, '添加服务器'),
1322
+ }, '添加服务器'),
1153
1323
  )
1154
1324
  children.push(header)
1155
1325
 
@@ -1189,6 +1359,15 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1189
1359
  detailChildren.push(createElement('span', { key: 'warn', className: 'mcp-test-warn', title: r.warning },
1190
1360
  '⚠️ ' + r.warning))
1191
1361
  }
1362
+ // Cached probe: label when it was taken so a restored status is never
1363
+ // mistaken for a just-run check. Clicking 🔌 测试 refreshes it.
1364
+ if (typeof testState.at === 'number') {
1365
+ detailChildren.push(createElement('span', {
1366
+ key: 'cached-at',
1367
+ className: 'mcp-test-cached',
1368
+ title: '上次检测结果(本地缓存)。点击「🔌 测试」可重新检测。',
1369
+ }, '· 缓存于 ' + formatTestTime(testState.at)))
1370
+ }
1192
1371
  testIndicator = createElement('div', {
1193
1372
  className: 'mcp-test mcp-test-list ' + (ok ? 'mcp-test-ok' : 'mcp-test-fail'),
1194
1373
  key: 'test-result',
@@ -1210,7 +1389,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1210
1389
  type: 'button',
1211
1390
  className: 'btn sm',
1212
1391
  disabled: view.mcpBusy || (testState !== null && testState.busy) || entry.config === null || entry.config === undefined,
1213
- title: entry.config === null || entry.config === undefined ? '该配置无法安全解析,请手动编辑 cordis.patch.yml' : (testState !== null && testState.busy ? '检测中...' : '测试该服务器的连通性'),
1392
+ title: entry.config === null || entry.config === undefined ? '该配置无法安全解析,请手动编辑 cordis.patch.yml' : (testState !== null && testState.busy ? '检测中...' : (testState !== null && typeof testState.at === 'number' ? '重新检测该服务器的连通性(当前显示的是缓存结果)' : '测试该服务器的连通性')),
1214
1393
  onClick: function (id) { return function () { testMcpEntry(id) } }(entry.id),
1215
1394
  }, testState !== null && testState.busy ? '检测中' : '🔌 测试'),
1216
1395
  createElement('button', {
@@ -1265,7 +1444,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1265
1444
  className: 'btn sm',
1266
1445
  title: '重新生成一个随机 ID',
1267
1446
  onClick: function () { patchDraft({ id: generateMcpId(view.mcpEntries) }) },
1268
- }, '🔄') : null,
1447
+ }, '🎲') : null,
1269
1448
  ),
1270
1449
  ),
1271
1450
  createElement('div', { style: fieldStyle, key: 'f-server' },
@@ -1330,7 +1509,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1330
1509
  onChange: function (e) { patchDraft({ env: e.target.value, envChanged: true }) },
1331
1510
  }),
1332
1511
  ) : null,
1333
- createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 0', marginBottom: '4px', borderTop: '1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.06))', paddingTop: '8px' }, key: 'f-reconnect-header' },
1512
+ createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 0', marginBottom: '4px', borderTop: '1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3))', paddingTop: '8px' }, key: 'f-reconnect-header' },
1334
1513
  createElement('input', {
1335
1514
  type: 'checkbox',
1336
1515
  id: 'reconnect-toggle',
@@ -1338,7 +1517,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1338
1517
  onChange: function (e) { patchDraft({ reconnectEnabled: e.target.checked }) },
1339
1518
  style: { margin: '0' },
1340
1519
  }),
1341
- createElement('label', { htmlFor: 'reconnect-toggle', style: { fontSize: '12px', fontWeight: 500, cursor: 'pointer', color: 'var(--dsw-alias-label-primary, #111)' } }, '启用自动重连'),
1520
+ createElement('label', { htmlFor: 'reconnect-toggle', style: { fontSize: '12px', fontWeight: 500, cursor: 'pointer', color: 'var(--dsw-alias-label-primary, #333)' } }, '启用自动重连'),
1342
1521
  ),
1343
1522
  d.reconnectEnabled ? createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '8px', paddingLeft: '4px' }, key: 'f-reconnect-fields' },
1344
1523
  createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 120px' }, key: 'f-reconnect-id' },
@@ -1391,7 +1570,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1391
1570
  children.push(editor)
1392
1571
  }
1393
1572
 
1394
- return createElement('div', { key: 'mcp-section', style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px', borderTop: '1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))', paddingTop: '8px' } }, children)
1573
+ return createElement('div', { key: 'mcp-section', style: { display: 'flex', flexDirection: 'column', gap: '10px' } }, children)
1395
1574
  }
1396
1575
 
1397
1576
  // Characters allowed in an MCP entry id: [A-Za-z0-9_-]. The random part uses
@@ -1623,15 +1802,24 @@ function buildSessionGroups(filtered, workspaceOrder) {
1623
1802
  function renderSessionsView(view, patch, reload, act) {
1624
1803
  var elements = []
1625
1804
 
1626
- // Toolbar
1805
+ // Toolbar: full-width search with inline icon; the refresh action sits at
1806
+ // the row's end so the search field gets the whole width.
1627
1807
  elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
1628
- createElement('div', { className: 'input-wrap', key: 'wrap' },
1808
+ createElement('div', { className: 'search-wrap session-search', key: 'wrap' },
1809
+ createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
1629
1810
  createElement('input', {
1630
1811
  className: 'input',
1631
1812
  placeholder: '搜索标题、内容摘要、目录或 Session ID...',
1632
1813
  value: view.needle,
1633
1814
  onChange: function (e) { patch({ needle: e.target.value }) },
1634
- })
1815
+ }),
1816
+ view.needle !== '' ? createElement('button', {
1817
+ type: 'button',
1818
+ key: 'btn-clear-search',
1819
+ className: 'btn sm',
1820
+ title: '清空搜索',
1821
+ onClick: function () { patch({ needle: '' }) },
1822
+ }, '✕') : null
1635
1823
  ),
1636
1824
  createElement('button', {
1637
1825
  type: 'button',
@@ -1641,7 +1829,7 @@ function renderSessionsView(view, patch, reload, act) {
1641
1829
  onClick: reload,
1642
1830
  },
1643
1831
  view.busy ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
1644
- '刷新'
1832
+ '🔄 刷新'
1645
1833
  )
1646
1834
  ))
1647
1835
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-admin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "dsh web UI plugin: unified administration panel — plugin and session management with modern tabbed Web UI (host remote + browser panel)",
6
6
  "main": "lib/index.js",
@@ -16,7 +16,7 @@
16
16
  "cordis"
17
17
  ],
18
18
  "scripts": {
19
- "test": "node scripts/self-check.mjs && node scripts/host-check.mjs"
19
+ "test": "node scripts/self-check.mjs && node scripts/host-check.mjs && node scripts/verify-mcp-cache.mjs"
20
20
  },
21
21
  "exports": {
22
22
  ".": "./lib/index.js",