dsh-plugin-admin 0.2.1 → 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 +261 -65
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -21,90 +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] .search-wrap { flex: 1.6; min-width: 0; position: relative; display: flex; align-items: center; }',
34
- '[data-dsh-admin-section] .search-wrap .input { flex: 1; min-width: 0; padding-left: 30px; }',
35
- '[data-dsh-admin-section] .install-wrap { flex: 1; min-width: 0; display: flex; align-items: center; }',
36
- '[data-dsh-admin-section] .install-wrap .input { flex: 1; min-width: 0; }',
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)); }',
37
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; }',
38
- '[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; }',
39
- '[data-dsh-admin-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.28)); }',
40
- '[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); }',
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); }',
41
44
  '[data-dsh-admin-section] .filter-bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }',
42
- '[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; }',
43
- '[data-dsh-admin-section] .pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.04)); }',
44
- '[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; }',
45
- '[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; }',
46
- '[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); }',
47
52
  '[data-dsh-admin-section] .btn:active:not(:disabled) { transform: translateY(1px); }',
48
- '[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; }',
49
54
  '[data-dsh-admin-section] .btn:disabled { opacity: 0.45; cursor: not-allowed; }',
50
- '[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; }',
51
- '[data-dsh-admin-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, #4096ff); }',
52
- '[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)); }',
53
- '[data-dsh-admin-section] .btn.danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.1)); }',
54
- '[data-dsh-admin-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #ef4444); color: #fff; border-color: transparent; }',
55
- '[data-dsh-admin-section] .btn.danger-solid:hover:not(:disabled) { opacity: 0.9; }',
56
- '[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; }',
57
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; }',
58
- '[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; }',
59
66
  '[data-dsh-admin-section] .list::-webkit-scrollbar { width: 6px; }',
60
- '[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)); }',
61
68
  '[data-dsh-admin-section] .session-group { display: flex; flex-direction: column; gap: 6px; }',
62
- '[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)); }',
63
- '[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; }',
64
71
  '[data-dsh-admin-section] .group-title { display: inline-flex; align-items: center; gap: 6px; }',
65
- '[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; }',
66
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; }',
67
- '[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; }',
68
- '[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); }',
69
78
  '[data-dsh-admin-section] .card-header { display: flex; align-items: center; gap: 8px; min-width: 0; }',
70
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; }',
71
80
  '[data-dsh-admin-section] .card-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }',
72
- '[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; }',
73
82
  '[data-dsh-admin-section] .summary-icon { font-size: 11px; line-height: 1.45; flex: none; opacity: 0.85; }',
74
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; }',
75
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; }',
76
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; }',
77
86
  '[data-dsh-admin-section] .card-actions { display: flex; gap: 5px; align-items: center; flex: none; }',
78
- '[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); }',
79
88
  '[data-dsh-admin-section] .tag.plugin { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); }',
80
- '[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); }',
81
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); }',
82
91
  '[data-dsh-admin-section] .tag.update-error { background: rgba(239, 68, 68, 0.1); color: var(--dsw-alias-state-error-primary, #ef4444); }',
83
- '[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)); }',
84
93
  '[data-dsh-admin-section] .tag.version { font-family: monospace; }',
85
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; }',
86
95
  '[data-dsh-admin-section] .tag.archived { background: rgba(245, 158, 11, 0.12); color: var(--dsw-alias-state-warn-label, #f59e0b); }',
87
- '[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; }',
88
97
  '[data-dsh-admin-section] .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dsw-alias-label-tertiary, #999); }',
89
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; }',
90
99
  '[data-dsh-admin-section] .dot.archived { background: var(--dsw-alias-state-warn-label, #f59e0b); }',
91
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); }',
92
101
  '[data-dsh-admin-section] .confirm-text { font-size: 11px; color: var(--dsw-alias-state-error-primary, #ef4444); font-weight: 500; }',
93
102
  '[data-dsh-admin-section] .confirm-actions { display: flex; gap: 5px; flex: none; }',
94
- '[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); }',
95
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; }',
96
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; }',
97
- '[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; }',
98
113
  '[data-dsh-admin-section] .footer .path { max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
99
114
  '[data-dsh-admin-section] .footer .hint { font-style: normal; }',
100
115
  '[data-dsh-admin-section] .session-id-badge { font-family: monospace; font-size: 10px; opacity: 0.75; }',
101
- '[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; }',
102
117
  '[data-dsh-admin-section] .mcp-test { display: inline-flex; align-items: center; gap: 5px; }',
103
118
  '[data-dsh-admin-section] .mcp-test.mcp-test-list { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }',
104
119
  '[data-dsh-admin-section] .mcp-test-busy { color: var(--dsw-alias-label-secondary, #666); }',
105
120
  '[data-dsh-admin-section] .mcp-test-ok { color: #16a34a; }',
106
121
  '[data-dsh-admin-section] .mcp-test-fail { color: var(--dsw-alias-state-error-primary, #ef4444); }',
107
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); }',
108
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%; } }',
109
125
  '@keyframes dsh-toast-in { 0% { opacity: 0; transform: translateY(-12px) scale(0.96); } 100% { opacity: 1; transform: translateY(0) scale(1); } }',
110
126
  '@keyframes dsh-toast-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-8px) scale(0.96); } }',
@@ -414,7 +430,7 @@ function setupMenuInjection(call, refreshSessions) {
414
430
  var sep = document.createElement('div')
415
431
  sep.setAttribute('role', 'separator')
416
432
  sep.setAttribute('data-dsh-admin-injected', '')
417
- 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))'
418
434
  viewport.appendChild(sep)
419
435
 
420
436
  var btn = document.createElement('button')
@@ -423,7 +439,7 @@ function setupMenuInjection(call, refreshSessions) {
423
439
  btn.setAttribute('data-dsh-admin-injected', '')
424
440
  btn.textContent = label
425
441
  var color = kind === 'danger' ? '#ef4444' : 'inherit'
426
- 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)'
427
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'
428
444
  btn.addEventListener('mouseenter', function () { btn.style.background = hoverBg })
429
445
  btn.addEventListener('mouseleave', function () { btn.style.background = 'transparent' })
@@ -493,6 +509,7 @@ function PluginsSection(props) {
493
509
  filter: 'all',
494
510
  needle: '',
495
511
  checkingUpdates: false,
512
+ updateChecked: false, // true once a check finished (even if no updates)
496
513
  updates: {}, // name -> { latest, updateAvailable, error? }
497
514
  })
498
515
  var pView = pairPlugins[0]
@@ -550,17 +567,24 @@ function PluginsSection(props) {
550
567
  var list = (result.value && result.value.updates) || []
551
568
  var map = {}
552
569
  var updateCount = 0
570
+ var checkedCount = 0
571
+ var checkErrorCount = 0
553
572
  for (var i = 0; i < list.length; i++) {
554
573
  var u = list[i]
555
574
  map[u.name] = { latest: u.latest, updateAvailable: u.updateAvailable === true, error: u.error || '' }
556
575
  if (u.updateAvailable === true) updateCount++
576
+ if (u.error) checkErrorCount++
577
+ checkedCount++
557
578
  }
558
579
  var updated = map
559
580
  patchPlugin({
560
581
  updates: updated,
582
+ updateChecked: true,
561
583
  note: updateCount > 0
562
584
  ? '发现 ' + updateCount + ' 个插件有新版本'
563
- : (list.length > 0 ? '已检查 ' + list.length + ' 个插件,均为最新版本' : ''),
585
+ : (checkErrorCount > 0
586
+ ? '已检查 ' + checkedCount + ' 个插件(' + checkErrorCount + ' 个查询失败),其余均为最新版本'
587
+ : (checkedCount > 0 ? '已检查 ' + checkedCount + ' 个插件,均为最新版本' : '')),
564
588
  })
565
589
  }, function (failure) {
566
590
  if (!alive.current) return
@@ -739,6 +763,101 @@ function SessionsSection(props) {
739
763
  /**
740
764
  * MCP configuration settings section (standalone, no internal tabs).
741
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
+
742
861
  function McpSection(props) {
743
862
  var pairMcp = useState({
744
863
  mcpEntries: [],
@@ -746,7 +865,9 @@ function McpSection(props) {
746
865
  mcpError: '',
747
866
  mcpEditorOpen: false,
748
867
  mcpDraft: null,
749
- 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(),
750
871
  })
751
872
  var mView = pairMcp[0]
752
873
  var setMView = pairMcp[1]
@@ -782,7 +903,16 @@ function McpSection(props) {
782
903
  callRemote('mcpAdmin/list', {}).then(function (result) {
783
904
  if (!alive.current) return
784
905
  if (result.ok) {
785
- 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
+ })
786
916
  } else {
787
917
  patchMcp({ mcpBusy: false, mcpError: '加载 MCP 配置失败:' + messageOf(result.error) })
788
918
  }
@@ -905,6 +1035,10 @@ function McpSection(props) {
905
1035
  callRemote('mcpAdmin/upsert', { entry: { id: draft.id, config: config } }).then(function (result) {
906
1036
  if (!alive.current) return
907
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 })
908
1042
  patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [], mcpEditorOpen: false, mcpDraft: null })
909
1043
  } else {
910
1044
  patchMcp({ mcpBusy: false, mcpError: '保存 MCP 配置失败:' + messageOf(result.error) })
@@ -920,6 +1054,8 @@ function McpSection(props) {
920
1054
  callRemote('mcpAdmin/remove', { id: id }).then(function (result) {
921
1055
  if (!alive.current) return
922
1056
  if (result.ok) {
1057
+ // The entry is gone — its cached probe must not linger in storage.
1058
+ clearMcpTestCacheEntry(id)
923
1059
  patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [] })
924
1060
  } else {
925
1061
  patchMcp({ mcpBusy: false, mcpError: '移除 MCP 配置失败:' + messageOf(result.error) })
@@ -930,12 +1066,18 @@ function McpSection(props) {
930
1066
  })
931
1067
  }
932
1068
 
933
- /** 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
+ */
934
1074
  function patchMcpTest(id, partial) {
935
1075
  setMView(function (cur) {
1076
+ var nextTestState = mergeTestState(cur.mcpTestState, id, partial)
1077
+ saveMcpTestCache(nextTestState)
936
1078
  var next = {}
937
1079
  for (var k in cur) next[k] = cur[k]
938
- next.mcpTestState = mergeTestState(cur.mcpTestState, id, partial)
1080
+ next.mcpTestState = nextTestState
939
1081
  return next
940
1082
  })
941
1083
  }
@@ -946,7 +1088,8 @@ function McpSection(props) {
946
1088
  callRemote('mcpAdmin/test', { id: id }).then(function (result) {
947
1089
  if (!alive.current) return
948
1090
  if (result.ok && result.value !== null && typeof result.value === 'object') {
949
- 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() })
950
1093
  } else {
951
1094
  patchMcpTest(id, { busy: false, result: null, error: messageOf(result.error) })
952
1095
  }
@@ -1049,7 +1192,7 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1049
1192
  onClick: checkUpdates,
1050
1193
  },
1051
1194
  view.checkingUpdates ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
1052
- view.checkingUpdates ? '检查中...' : ' 检查更新'
1195
+ view.checkingUpdates ? '检查中...' : '⬆️ 检查更新'
1053
1196
  ),
1054
1197
  ))
1055
1198
 
@@ -1094,6 +1237,41 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1094
1237
  elements.push(createElement('div', { className: 'error', key: 'error' }, view.error))
1095
1238
  }
1096
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
+
1097
1275
  // Filter and render rows
1098
1276
  var needle = view.needle.trim().toLowerCase()
1099
1277
  var filtered = filterPlugins(view.plugins, view.filter, needle)
@@ -1125,23 +1303,23 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1125
1303
 
1126
1304
  function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry) {
1127
1305
  var children = []
1128
- var header = createElement('div', { className: 'group-header', key: 'mcp-header' },
1306
+ var header = createElement('div', { className: 'group-header', key: 'mcp-header', style: { marginTop: 0 } },
1129
1307
  createElement('span', { className: 'group-title', key: 't' }, '🔌 MCP 配置'),
1130
1308
  createElement('span', { className: 'group-count', key: 'c' }, String(view.mcpEntries.length) + ' 个'),
1131
1309
  createElement('button', {
1132
1310
  type: 'button',
1133
1311
  key: 'btn-refresh-mcp',
1134
- className: 'btn sm',
1312
+ className: 'btn',
1135
1313
  disabled: view.mcpBusy,
1136
1314
  onClick: reloadMcp,
1137
- }, '刷新'),
1315
+ }, '🔄 刷新'),
1138
1316
  createElement('button', {
1139
1317
  type: 'button',
1140
1318
  key: 'btn-add-mcp',
1141
- className: 'btn primary sm',
1319
+ className: 'btn primary',
1142
1320
  disabled: view.mcpBusy,
1143
1321
  onClick: function () { openMcpEditor(null) },
1144
- }, '添加服务器'),
1322
+ }, '添加服务器'),
1145
1323
  )
1146
1324
  children.push(header)
1147
1325
 
@@ -1181,6 +1359,15 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1181
1359
  detailChildren.push(createElement('span', { key: 'warn', className: 'mcp-test-warn', title: r.warning },
1182
1360
  '⚠️ ' + r.warning))
1183
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
+ }
1184
1371
  testIndicator = createElement('div', {
1185
1372
  className: 'mcp-test mcp-test-list ' + (ok ? 'mcp-test-ok' : 'mcp-test-fail'),
1186
1373
  key: 'test-result',
@@ -1202,7 +1389,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1202
1389
  type: 'button',
1203
1390
  className: 'btn sm',
1204
1391
  disabled: view.mcpBusy || (testState !== null && testState.busy) || entry.config === null || entry.config === undefined,
1205
- 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' ? '重新检测该服务器的连通性(当前显示的是缓存结果)' : '测试该服务器的连通性')),
1206
1393
  onClick: function (id) { return function () { testMcpEntry(id) } }(entry.id),
1207
1394
  }, testState !== null && testState.busy ? '检测中' : '🔌 测试'),
1208
1395
  createElement('button', {
@@ -1257,7 +1444,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1257
1444
  className: 'btn sm',
1258
1445
  title: '重新生成一个随机 ID',
1259
1446
  onClick: function () { patchDraft({ id: generateMcpId(view.mcpEntries) }) },
1260
- }, '🔄') : null,
1447
+ }, '🎲') : null,
1261
1448
  ),
1262
1449
  ),
1263
1450
  createElement('div', { style: fieldStyle, key: 'f-server' },
@@ -1322,7 +1509,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1322
1509
  onChange: function (e) { patchDraft({ env: e.target.value, envChanged: true }) },
1323
1510
  }),
1324
1511
  ) : null,
1325
- 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' },
1326
1513
  createElement('input', {
1327
1514
  type: 'checkbox',
1328
1515
  id: 'reconnect-toggle',
@@ -1330,7 +1517,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1330
1517
  onChange: function (e) { patchDraft({ reconnectEnabled: e.target.checked }) },
1331
1518
  style: { margin: '0' },
1332
1519
  }),
1333
- 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)' } }, '启用自动重连'),
1334
1521
  ),
1335
1522
  d.reconnectEnabled ? createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '8px', paddingLeft: '4px' }, key: 'f-reconnect-fields' },
1336
1523
  createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 120px' }, key: 'f-reconnect-id' },
@@ -1383,7 +1570,7 @@ function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEd
1383
1570
  children.push(editor)
1384
1571
  }
1385
1572
 
1386
- 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)
1387
1574
  }
1388
1575
 
1389
1576
  // Characters allowed in an MCP entry id: [A-Za-z0-9_-]. The random part uses
@@ -1615,15 +1802,24 @@ function buildSessionGroups(filtered, workspaceOrder) {
1615
1802
  function renderSessionsView(view, patch, reload, act) {
1616
1803
  var elements = []
1617
1804
 
1618
- // 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.
1619
1807
  elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
1620
- 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' }, '🔍'),
1621
1810
  createElement('input', {
1622
1811
  className: 'input',
1623
1812
  placeholder: '搜索标题、内容摘要、目录或 Session ID...',
1624
1813
  value: view.needle,
1625
1814
  onChange: function (e) { patch({ needle: e.target.value }) },
1626
- })
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
1627
1823
  ),
1628
1824
  createElement('button', {
1629
1825
  type: 'button',
@@ -1633,7 +1829,7 @@ function renderSessionsView(view, patch, reload, act) {
1633
1829
  onClick: reload,
1634
1830
  },
1635
1831
  view.busy ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
1636
- '刷新'
1832
+ '🔄 刷新'
1637
1833
  )
1638
1834
  ))
1639
1835
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-admin",
3
- "version": "0.2.1",
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",