openclacky 1.3.10 → 1.4.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.
@@ -607,9 +607,22 @@ module Clacky
607
607
  ""
608
608
  )
609
609
 
610
- # Pass 1: anchored strip — the full wrapper echoed at the start,
611
- # possibly spanning multiple real newlines.
612
- text = text.sub(/\A\{.*?"\$__clacky_ec"\s*\n?/m, "")
610
+ # Pass 1: anchored strip — remove ONLY the wrapper's echoed head
611
+ # line (`{ USER_CMD`) at the very start of the buffer, and ONLY
612
+ # when a matching wrapper tail fingerprint (`}...; __clacky_ec=$?`)
613
+ # actually exists somewhere in the buffer. The guard is what keeps
614
+ # us from eating a real first line of user output that merely
615
+ # happens to start with `{` (e.g. JSON like `{"key": ...}`).
616
+ # We test the tail with a separate anchor-free `match?` (linear, no
617
+ # backtracking) rather than a lookahead, to avoid catastrophic
618
+ # regex backtracking on large `{`-prefixed output. We strip a
619
+ # single line (`[^\n]*`, not cross-line `.*?`): a multi-line command
620
+ # echoed in cooked mode puts the wrapper tail *after* the real
621
+ # output, so cross-line greed would swallow that output whole. The
622
+ # tail itself is removed by the fingerprint passes below.
623
+ if text.start_with?("{") && text.match?(/\}[^\n;]*; *__clacky_ec=\$\?/)
624
+ text = text.sub(/\A\{[^\n]*\n/, "")
625
+ end
613
626
 
614
627
  # Pass 2: token-aware global strip — remove any leftover wrapper
615
628
  # echo fragment, wherever it sits. Requires the session token so
@@ -629,9 +642,11 @@ module Clacky
629
642
  # 2b. Single-line shape: everything collapsed onto one line.
630
643
  # Strip from the wrapper's `}; __clacky_ec=$?` pivot (or the
631
644
  # opening `{` if still present on that line) through the end of
632
- # the printf invocation (`"$__clacky_ec"`).
645
+ # the printf invocation (`"$__clacky_ec"`). The optional
646
+ # `[^\n;]*` between `}` and `;` absorbs a WSL `</dev/null`
647
+ # stdin redirect spliced onto the group.
633
648
  text = text.gsub(
634
- /[^\n]*\}; *__clacky_ec=\$\?; *printf[^\n]*__CLACKY_DONE_#{token_re}_[^\n]*"\$__clacky_ec"[^\n]*\n?/,
649
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?; *printf[^\n]*__CLACKY_DONE_#{token_re}_[^\n]*"\$__clacky_ec"[^\n]*\n?/,
635
650
  ""
636
651
  )
637
652
 
@@ -653,9 +668,10 @@ module Clacky
653
668
  # private double-underscore var name that user code effectively
654
669
  # never emits — so we strip anything between them (non-greedy,
655
670
  # multiline-aware) to also handle width-wrap that inserted
656
- # real \n breaks inside the echo.
671
+ # real \n breaks inside the echo. `[^\n;]*` between `}` and `;`
672
+ # absorbs a WSL `</dev/null` stdin redirect on the group.
657
673
  text = text.gsub(
658
- /[^\n]*\}; *__clacky_ec=\$\?.*?"\$__clacky_ec"[^\n]*\n?/m,
674
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?.*?"\$__clacky_ec"[^\n]*\n?/m,
659
675
  ""
660
676
  )
661
677
 
@@ -663,7 +679,7 @@ module Clacky
663
679
  # truncation cut off everything after `__clacky_ec=$?`). Still a
664
680
  # reliable fingerprint thanks to the `__clacky_ec` var name.
665
681
  text = text.gsub(
666
- /[^\n]*\}; *__clacky_ec=\$\?;?[^\n]*\n?/,
682
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?;?[^\n]*\n?/,
667
683
  ""
668
684
  )
669
685
 
@@ -402,6 +402,10 @@ module Clacky
402
402
  screen.show_cursor
403
403
  screen.flush
404
404
  end
405
+ # Restore cooked mode so the terminal is usable after exit, regardless
406
+ # of which exit path was taken (some call exit(0) and bypass the
407
+ # input_loop ensure). Idempotent; safe to call more than once.
408
+ screen.disable_raw_mode
405
409
  end
406
410
 
407
411
  # /clear: wipe output area + buffer, keep fixed area.
@@ -118,14 +118,42 @@ module Clacky
118
118
  @height = TTY::Screen.height
119
119
  end
120
120
 
121
- # Enable raw mode (disable line buffering)
121
+ # Enable raw mode (disable line buffering). The very first time it runs it
122
+ # snapshots the terminal's original settings (via `stty -g`) so the exact
123
+ # pre-clacky state can be restored on exit — `IO#cooked!` only applies a
124
+ # generic cooked profile and does not restore flow-control (IXON) and other
125
+ # bits, which on macOS leaves the shell in a broken input state.
122
126
  def enable_raw_mode
127
+ return unless $stdin.tty?
128
+
129
+ @original_stty ||= capture_stty
123
130
  $stdin.raw!
124
131
  end
125
132
 
126
- # Disable raw mode
133
+ # Disable raw mode. Idempotent and tty-guarded so it can be safely called
134
+ # from any exit path. Restores the exact original terminal settings when a
135
+ # snapshot exists, then flushes pending input so stray bytes don't leak to
136
+ # the shell. Falls back to `IO#cooked!` when no snapshot is available.
127
137
  def disable_raw_mode
128
- $stdin.cooked!
138
+ return unless $stdin.tty?
139
+
140
+ $stdin.cooked! unless @original_stty && restore_stty(@original_stty)
141
+ $stdin.ioflush
142
+ rescue IOError, Errno::ENOTTY
143
+ nil
144
+ end
145
+
146
+ private def capture_stty
147
+ out = Clacky::Utils::Encoding.cmd_to_utf8(`stty -g 2>/dev/null`).strip
148
+ out.empty? ? nil : out
149
+ rescue StandardError
150
+ nil
151
+ end
152
+
153
+ private def restore_stty(state)
154
+ system("stty", state, out: File::NULL, err: File::NULL)
155
+ rescue StandardError
156
+ false
129
157
  end
130
158
 
131
159
  # Read a single character without echo
@@ -604,8 +604,7 @@ module Clacky
604
604
 
605
605
  ext = File.extname(path).downcase
606
606
  if LOCAL_MEDIA_EXTENSIONS.include?(ext) && File.exist?(path)
607
- encoded = CGI.escape(href)
608
- "![#{alt}](/api/local-image?path=#{encoded})"
607
+ "![#{alt}](#{local_image_proxy_url(href, path)})"
609
608
  else
610
609
  _match
611
610
  end
@@ -622,8 +621,7 @@ module Clacky
622
621
 
623
622
  ext = File.extname(path).downcase
624
623
  if LOCAL_VIDEO_EXTENSIONS.include?(ext) && File.exist?(path)
625
- encoded = CGI.escape(href)
626
- "<video#{pre} src=\"/api/local-image?path=#{encoded}\"#{post}>"
624
+ "<video#{pre} src=\"#{local_image_proxy_url(href, path)}\"#{post}>"
627
625
  else
628
626
  _match
629
627
  end
@@ -640,8 +638,7 @@ module Clacky
640
638
 
641
639
  ext = File.extname(path).downcase
642
640
  if LOCAL_AUDIO_EXTENSIONS.include?(ext) && File.exist?(path)
643
- encoded = CGI.escape(href)
644
- "<audio#{pre} src=\"/api/local-image?path=#{encoded}\"#{post}>"
641
+ "<audio#{pre} src=\"#{local_image_proxy_url(href, path)}\"#{post}>"
645
642
  else
646
643
  _match
647
644
  end
@@ -658,8 +655,7 @@ module Clacky
658
655
  ext = File.extname(path).downcase
659
656
  if LOCAL_VIDEO_EXTENSIONS.include?(ext) || LOCAL_AUDIO_EXTENSIONS.include?(ext)
660
657
  if File.exist?(path)
661
- encoded = CGI.escape(href)
662
- "[#{text}](/api/local-image?path=#{encoded})"
658
+ "[#{text}](#{local_image_proxy_url(href, path)})"
663
659
  else
664
660
  _match
665
661
  end
@@ -670,6 +666,15 @@ module Clacky
670
666
 
671
667
  content
672
668
  end
669
+
670
+ # Build a proxy URL for a local media file, appending a version param
671
+ # derived from the file mtime so an overwritten same-name file produces a
672
+ # different URL and defeats the browser's in-memory image cache.
673
+ def self.local_image_proxy_url(href, path)
674
+ encoded = CGI.escape(href)
675
+ version = File.mtime(path).to_i
676
+ "/api/local-image?path=#{encoded}&v=#{version}"
677
+ end
673
678
  end
674
679
  end
675
680
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.3.10"
4
+ VERSION = "1.4.0"
5
5
  end
@@ -782,7 +782,7 @@ body {
782
782
  color: var(--color-accent-primary);
783
783
  }
784
784
 
785
- /* ── Command-palette search overlay (⌘K) ─────────────────────────────────── */
785
+ /* ── Command-palette search overlay ──────────────────────────────────────── */
786
786
  .cmd-palette-overlay {
787
787
  position: fixed;
788
788
  inset: 0;
@@ -6484,6 +6484,28 @@ body {
6484
6484
  overflow-y: auto;
6485
6485
  }
6486
6486
 
6487
+ /* ── Skills search ──────────────────────────────────────────────────────── */
6488
+ .skills-search {
6489
+ font-size: 0.8125rem;
6490
+ padding: 0.375rem 0.75rem;
6491
+ border-radius: 6px;
6492
+ border: 1px solid var(--color-border-primary);
6493
+ background: var(--color-bg-secondary);
6494
+ color: var(--color-text-primary);
6495
+ width: 12rem;
6496
+ }
6497
+ .skills-search::placeholder { color: var(--color-text-tertiary); }
6498
+ .skills-search:focus {
6499
+ outline: none;
6500
+ border-color: var(--color-accent, #6366f1);
6501
+ }
6502
+ .skills-search-empty {
6503
+ padding: 2rem 1rem;
6504
+ text-align: center;
6505
+ color: var(--color-text-tertiary);
6506
+ font-size: 0.875rem;
6507
+ }
6508
+
6487
6509
  /* ── My Skills upload area (user-licensed only) ───────────────────────────── */
6488
6510
  .my-skills-upload-area {
6489
6511
  display: flex;
@@ -7072,6 +7094,12 @@ body {
7072
7094
  }
7073
7095
 
7074
7096
  /* ── Extension Market ─────────────────────────────────────────────────── */
7097
+ #extensions-panel {
7098
+ flex: 1;
7099
+ display: flex;
7100
+ flex-direction: column;
7101
+ overflow: hidden;
7102
+ }
7075
7103
  #extensions-body {
7076
7104
  flex: 1;
7077
7105
  overflow-y: auto;
@@ -7165,26 +7193,38 @@ body {
7165
7193
  background: var(--color-bg-secondary);
7166
7194
  border: 1px solid var(--color-border-primary);
7167
7195
  border-radius: 8px;
7168
- padding: 0.875rem 1rem;
7196
+ padding: 0.6rem 1rem;
7169
7197
  transition: border-color .15s;
7170
7198
  }
7171
7199
  .extension-card:hover { border-color: var(--color-text-muted); }
7172
7200
  .extension-card-main {
7173
7201
  display: flex;
7174
- align-items: center;
7202
+ align-items: stretch;
7175
7203
  gap: 0.75rem;
7176
7204
  }
7177
7205
  .extension-emoji {
7178
7206
  font-size: 1.5rem;
7179
7207
  line-height: 1.4;
7180
7208
  flex-shrink: 0;
7209
+ display: flex;
7210
+ align-items: flex-start;
7211
+ }
7212
+ .extension-default-icon {
7213
+ width: 2rem;
7214
+ height: 100%;
7215
+ display: block;
7216
+ border-radius: 7px;
7217
+ }
7218
+ .extension-emoji-lg .extension-default-icon {
7219
+ width: 2.75rem;
7220
+ height: 2.75rem;
7221
+ border-radius: 10px;
7181
7222
  }
7182
- .extension-info { flex: 1; min-width: 0; }
7223
+ .extension-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
7183
7224
  .extension-title {
7184
7225
  display: flex;
7185
7226
  align-items: center;
7186
- gap: 0.375rem;
7187
- margin-bottom: 0.25rem;
7227
+ gap: 0.5rem;
7188
7228
  flex-wrap: wrap;
7189
7229
  }
7190
7230
  .extension-name {
@@ -7197,18 +7237,24 @@ body {
7197
7237
  color: var(--color-text-secondary);
7198
7238
  background: var(--color-bg-tertiary, rgba(255,255,255,0.05));
7199
7239
  border-radius: 4px;
7200
- padding: 0.0625rem 0.375rem;
7201
7240
  }
7202
7241
  .extension-units {
7203
7242
  font-size: 0.6875rem;
7204
7243
  color: var(--color-text-muted);
7205
7244
  }
7206
7245
  .extension-installed {
7207
- font-size: 0.6875rem;
7246
+ font-size: 0.625rem;
7208
7247
  color: var(--color-success, #16a34a);
7209
7248
  background: var(--color-success-bg, rgba(22,163,74,0.12));
7210
7249
  border-radius: 4px;
7211
- padding: 0.0625rem 0.375rem;
7250
+ padding: 0.2rem 0.375rem;
7251
+ }
7252
+ .extension-updatable {
7253
+ font-size: 0.625rem;
7254
+ color: var(--color-button-primary);
7255
+ background: color-mix(in srgb, var(--color-button-primary) 12%, transparent);
7256
+ border-radius: 4px;
7257
+ padding: 0.2rem 0.375rem;
7212
7258
  }
7213
7259
  .extension-unlisted {
7214
7260
  font-size: 0.6875rem;
@@ -7221,11 +7267,10 @@ body {
7221
7267
  font-size: 0.75rem;
7222
7268
  color: var(--color-text-secondary);
7223
7269
  line-height: 1.5;
7224
- margin-bottom: 0.375rem;
7225
7270
  }
7226
7271
  .extension-desc-detail {
7227
7272
  font-size: 0.8125rem;
7228
- margin-bottom: 0.5rem;
7273
+ margin-bottom: 0;
7229
7274
  }
7230
7275
  .extension-detail-author {
7231
7276
  font-size: 0.6875rem;
@@ -7236,6 +7281,11 @@ body {
7236
7281
  color: var(--color-text-muted);
7237
7282
  font-weight: 400;
7238
7283
  }
7284
+ .extension-installs {
7285
+ font-size: 0.75rem;
7286
+ color: var(--color-text-muted);
7287
+ font-weight: 400;
7288
+ }
7239
7289
  .extension-meta {
7240
7290
  display: flex;
7241
7291
  align-items: center;
@@ -7282,8 +7332,13 @@ body {
7282
7332
  padding-bottom: 1.25rem;
7283
7333
  border-bottom: 1px solid var(--color-border-primary);
7284
7334
  }
7285
- .extension-detail-heading { flex: 1; min-width: 0; }
7335
+ .extension-detail-heading { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
7286
7336
  .extension-emoji-lg { font-size: 2.5rem; line-height: 1; margin-top: 0.125rem; }
7337
+ .extension-emoji-lg .extension-default-icon {
7338
+ width: 3.5rem;
7339
+ height: 3.5rem;
7340
+ border-radius: 12px;
7341
+ }
7287
7342
  .extension-name-lg { font-size: 1.125rem; font-weight: 700; }
7288
7343
  .extension-detail-block { margin-top: 1.5rem; }
7289
7344
  .extension-detail-block-title {
@@ -7291,7 +7346,6 @@ body {
7291
7346
  font-weight: 600;
7292
7347
  text-transform: uppercase;
7293
7348
  letter-spacing: 0.04em;
7294
- color: var(--color-text-muted);
7295
7349
  margin: 0 0 0.75rem;
7296
7350
  }
7297
7351
  .extension-detail-section { margin-bottom: 0.875rem; }
@@ -7338,33 +7392,78 @@ body {
7338
7392
  margin-top: 0.125rem;
7339
7393
  }
7340
7394
  .extension-version-item {
7341
- padding: 0.625rem 0.875rem;
7395
+ padding: 0.75rem 1rem;
7342
7396
  border-bottom: 1px solid var(--color-border-primary);
7343
7397
  }
7344
7398
  .extension-version-item:last-child { border-bottom: none; }
7345
7399
  .extension-version-row {
7346
7400
  display: flex;
7347
- align-items: center;
7401
+ align-items: baseline;
7348
7402
  gap: 0.5rem;
7349
7403
  }
7404
+ .extension-version-item:has(.extension-version-notes) .extension-version-row {
7405
+ padding-bottom: 0.5rem;
7406
+ margin-bottom: 0.5rem;
7407
+ border-bottom: 1px solid var(--color-border-primary);
7408
+ }
7409
+ .extension-version-row .extension-version {
7410
+ font-size: 0.8375rem;
7411
+ font-weight: 700;
7412
+ color: var(--color-text-primary);
7413
+ background: none;
7414
+ padding: 0;
7415
+ border-radius: 0;
7416
+ }
7417
+ .extension-version-separator {
7418
+ font-size: 0.8125rem;
7419
+ color: var(--color-text-secondary);
7420
+ }
7350
7421
  .extension-version-date {
7351
7422
  font-size: 0.6875rem;
7352
7423
  color: var(--color-text-muted);
7353
7424
  }
7354
7425
  .extension-version-notes {
7355
- font-size: 0.75rem;
7426
+ font-size: 0.8125rem;
7356
7427
  color: var(--color-text-secondary);
7357
- line-height: 1.5;
7358
- margin-top: 0.25rem;
7359
- white-space: pre-wrap;
7428
+ line-height: 1.6;
7429
+ margin-top: 0.5rem;
7430
+ }
7431
+ .extension-version-notes h1,
7432
+ .extension-version-notes h2,
7433
+ .extension-version-notes h3 {
7434
+ font-size: 0.8125rem;
7435
+ font-weight: 600;
7436
+ color: var(--color-text-primary);
7437
+ margin: 0.75rem 0 0.25rem;
7438
+ }
7439
+ .extension-version-notes h1 {
7440
+ font-size: 0.9375rem;
7441
+ }
7442
+ .extension-version-notes h1:first-child,
7443
+ .extension-version-notes h2:first-child,
7444
+ .extension-version-notes h3:first-child {
7445
+ margin-top: 0;
7446
+ }
7447
+ .extension-version-notes ul,
7448
+ .extension-version-notes ol {
7449
+ margin: 0.25rem 0;
7450
+ padding-left: 1.25rem;
7360
7451
  }
7452
+ .extension-version-notes li {
7453
+ margin: 0.1rem 0;
7454
+ }
7455
+ .extension-version-notes p {
7456
+ margin: 0.25rem 0;
7457
+ }
7458
+ .extension-version-notes p:first-child { margin-top: 0; }
7459
+ .extension-version-notes p:last-child { margin-bottom: 0; }
7361
7460
  .extension-detail-loading { display: flex; flex-direction: column; gap: 0.5rem; }
7362
7461
 
7363
7462
  .extension-detail-actions {
7364
7463
  display: flex;
7365
7464
  align-items: center;
7366
7465
  gap: 0.5rem;
7367
- margin-top: 0.75rem;
7466
+ margin-top: 0.25rem;
7368
7467
  }
7369
7468
  .extension-disabled {
7370
7469
  font-size: 0.6875rem;
@@ -7399,6 +7498,18 @@ body {
7399
7498
  opacity: 0.5;
7400
7499
  cursor: not-allowed;
7401
7500
  }
7501
+ .extension-action-update {
7502
+ background: var(--color-button-primary);
7503
+ color: var(--color-button-primary-text);
7504
+ border-color: transparent;
7505
+ }
7506
+ .extension-action-update:hover {
7507
+ background: var(--color-button-primary-hover);
7508
+ }
7509
+ .extension-action-update:disabled {
7510
+ opacity: 0.5;
7511
+ cursor: not-allowed;
7512
+ }
7402
7513
  .extension-action-disable,
7403
7514
  .extension-action-enable {
7404
7515
  color: var(--color-text-tertiary);
@@ -7560,7 +7671,7 @@ body {
7560
7671
  color: #ffffff;
7561
7672
  border: 1px solid var(--color-accent-primary);
7562
7673
  }
7563
- .btn-brand-update:hover { background: var(--color-accent-hover); }
7674
+ .btn-brand-update:hover { background: var(--color-button-primary-hover); }
7564
7675
  .btn-brand-install:disabled,
7565
7676
  .btn-brand-update:disabled {
7566
7677
  opacity: 0.5;
@@ -9984,7 +10095,7 @@ body.setup-mode[data-theme="dark"] {
9984
10095
 
9985
10096
  .memory-card-actions {
9986
10097
  display: flex;
9987
- align-items: flex-start;
10098
+ align-items: center;
9988
10099
  gap: 0.375rem;
9989
10100
  flex-shrink: 0;
9990
10101
  }
@@ -10024,6 +10135,7 @@ body.setup-mode[data-theme="dark"] {
10024
10135
 
10025
10136
  .btn-memory-expand {
10026
10137
  min-width: 1.75rem;
10138
+ align-self: stretch;
10027
10139
  justify-content: center;
10028
10140
  color: var(--color-text-muted);
10029
10141
  }
@@ -10537,7 +10649,7 @@ body.setup-mode[data-theme="dark"] {
10537
10649
 
10538
10650
  /* ── Model Filter ────────────────────────────────────────────────────── */
10539
10651
  .billing-model-filter {
10540
- padding: 0.4375rem 0.75rem;
10652
+ padding: 0.4375rem 1.75rem 0.4375rem 0.75rem;
10541
10653
  border: 1px solid var(--color-border-primary);
10542
10654
  border-radius: 8px;
10543
10655
  background: var(--color-bg-secondary);
@@ -10545,6 +10657,23 @@ body.setup-mode[data-theme="dark"] {
10545
10657
  font-size: 0.8125rem;
10546
10658
  cursor: pointer;
10547
10659
  min-width: 130px;
10660
+ appearance: none;
10661
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
10662
+ background-repeat: no-repeat;
10663
+ background-position: right 0.625rem center;
10664
+ transition: border-color 0.15s ease;
10665
+ }
10666
+ .billing-model-filter:hover {
10667
+ border-color: var(--color-border-strong);
10668
+ }
10669
+ .billing-model-filter:focus {
10670
+ outline: none;
10671
+ border-color: var(--color-accent-primary);
10672
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
10673
+ }
10674
+ .billing-model-filter option {
10675
+ background: var(--color-bg-secondary);
10676
+ color: var(--color-text-primary);
10548
10677
  }
10549
10678
 
10550
10679
  /* ── Action Buttons ──────────────────────────────────────────────────── */
@@ -12327,11 +12456,48 @@ body.setup-mode[data-theme="dark"] {
12327
12456
  display: none;
12328
12457
  }
12329
12458
 
12459
+ /* Collapse session search into the right-side header actions */
12460
+ #header-center {
12461
+ justify-content: flex-end;
12462
+ padding: 0;
12463
+ }
12464
+ #header-cmdbar {
12465
+ flex: 0 0 1.875rem;
12466
+ width: 1.875rem;
12467
+ max-width: 1.875rem;
12468
+ padding: 0;
12469
+ border: none;
12470
+ border-radius: var(--radius-sm);
12471
+ background: transparent;
12472
+ justify-content: center;
12473
+ gap: 0;
12474
+ margin-right: 0.125rem;
12475
+ }
12476
+ #header-cmdbar:hover {
12477
+ color: var(--color-text-primary);
12478
+ }
12479
+ #header-cmdbar svg {
12480
+ width: 1rem;
12481
+ height: 1rem;
12482
+ opacity: 1;
12483
+ }
12484
+ #header-cmdbar .cmdbar-ph,
12485
+ #header-cmdbar .cmdbar-kbd,
12486
+ #ext-slot-header-right:empty {
12487
+ display: none;
12488
+ }
12489
+
12330
12490
  /* Tighten right-side icon gap on mobile */
12331
12491
  #header-right {
12332
12492
  gap: 0.125rem;
12333
12493
  }
12334
12494
 
12495
+ @media (max-width: 360px) {
12496
+ #top-header {
12497
+ padding: 0 0.375rem;
12498
+ }
12499
+ }
12500
+
12335
12501
  /* Session info bar: single-line, no hover-expand, font smaller */
12336
12502
  #session-info-bar {
12337
12503
  padding: 0.1875rem 0.5rem;
@@ -12748,4 +12914,3 @@ body.setup-mode[data-theme="dark"] {
12748
12914
  background: var(--color-bg-primary);
12749
12915
  }
12750
12916
  }
12751
-
@@ -66,6 +66,7 @@ const ExtensionsStore = (() => {
66
66
 
67
67
  /** Fetch the catalog from the server for the current query + sort. */
68
68
  async load() {
69
+ if (_filterInstalled) return;
69
70
  _loading = true;
70
71
  _error = null;
71
72
  _emit("extensions:loading");
@@ -79,12 +80,8 @@ const ExtensionsStore = (() => {
79
80
  _allExtensions = data.extensions || [];
80
81
  _error = data.warning || null;
81
82
  _loading = false;
82
- if (_filterInstalled) {
83
- await Extensions.setFilterInstalled(true);
84
- } else {
85
- _extensions = _allExtensions;
86
- _emit("extensions:changed", { extensions: _extensions, warning: _error });
87
- }
83
+ _extensions = _allExtensions;
84
+ _emit("extensions:changed", { extensions: _extensions, warning: _error });
88
85
  } catch (e) {
89
86
  console.error("[Extensions] load failed", e);
90
87
  _extensions = [];
@@ -99,6 +96,7 @@ const ExtensionsStore = (() => {
99
96
  /** Set the search text and reload. */
100
97
  setQuery(query) {
101
98
  _query = (query || "").trim();
99
+ if (_filterInstalled) return Extensions.setFilterInstalled(true);
102
100
  return Extensions.load();
103
101
  },
104
102
 
@@ -117,7 +115,11 @@ const ExtensionsStore = (() => {
117
115
  try {
118
116
  const res = await fetch("/api/store/extensions/installed");
119
117
  const data = await res.json();
120
- _extensions = data.extensions || [];
118
+ const all = data.extensions || [];
119
+ _extensions = _query
120
+ ? all.filter(e => [e.name, e.name_zh, e.description].some(
121
+ f => f && f.toLowerCase().includes(_query.toLowerCase())))
122
+ : all;
121
123
  } catch (e) {
122
124
  console.error("[Extensions] load installed failed", e);
123
125
  _extensions = [];
@@ -210,6 +212,11 @@ const ExtensionsStore = (() => {
210
212
  }
211
213
  },
212
214
 
215
+ /** Update an installed extension to the latest marketplace version. */
216
+ async update(id) {
217
+ return Extensions.install(id);
218
+ },
219
+
213
220
  /** Remove an installed extension, then return to the list. */
214
221
  async uninstall(id) {
215
222
  if (!id) return;