openclacky 1.5.1 → 1.5.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/lib/clacky/agent/llm_caller.rb +48 -0
- data/lib/clacky/agent/message_compressor_helper.rb +7 -0
- data/lib/clacky/agent/session_serializer.rb +42 -0
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +58 -1
- data/lib/clacky/agent_config.rb +71 -1
- data/lib/clacky/brand_config.rb +88 -59
- data/lib/clacky/client.rb +13 -2
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +15 -3
- data/lib/clacky/extension/packager.rb +17 -0
- data/lib/clacky/locales/en.rb +4 -2
- data/lib/clacky/locales/zh.rb +5 -3
- data/lib/clacky/message_format/anthropic.rb +56 -1
- data/lib/clacky/message_format/open_ai.rb +69 -4
- data/lib/clacky/message_history.rb +15 -1
- data/lib/clacky/providers.rb +73 -6
- data/lib/clacky/server/http_server.rb +61 -29
- data/lib/clacky/server/session_registry.rb +4 -2
- data/lib/clacky/skill.rb +4 -0
- data/lib/clacky/tools/invoke_skill.rb +21 -0
- data/lib/clacky/tools/security.rb +11 -13
- data/lib/clacky/ui_interface.rb +2 -0
- data/lib/clacky/utils/model_pricing.rb +8 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +210 -21
- data/lib/clacky/web/app.js +8 -3
- data/lib/clacky/web/components/code-editor.js +50 -12
- data/lib/clacky/web/features/billing/view.js +7 -1
- data/lib/clacky/web/features/extensions/store.js +23 -7
- data/lib/clacky/web/features/extensions/view.js +8 -4
- data/lib/clacky/web/features/new-session/view.js +61 -0
- data/lib/clacky/web/features/workspace/store.js +9 -0
- data/lib/clacky/web/features/workspace/view.js +8 -1
- data/lib/clacky/web/i18n.js +14 -0
- data/lib/clacky/web/index.html +4 -2
- data/lib/clacky/web/settings.js +49 -14
- metadata +2 -2
data/lib/clacky/web/app.css
CHANGED
|
@@ -2173,6 +2173,40 @@ body {
|
|
|
2173
2173
|
margin-top: -0.625rem;
|
|
2174
2174
|
padding-left: 0.125rem;
|
|
2175
2175
|
}
|
|
2176
|
+
|
|
2177
|
+
/* ── Starter Prompts ── */
|
|
2178
|
+
.new-session-starter-prompts {
|
|
2179
|
+
margin-top: 0.25rem;
|
|
2180
|
+
}
|
|
2181
|
+
.ns-starter-label {
|
|
2182
|
+
font-size: 0.75rem;
|
|
2183
|
+
color: var(--color-text-tertiary);
|
|
2184
|
+
margin-bottom: 0.5rem;
|
|
2185
|
+
}
|
|
2186
|
+
.ns-starter-list {
|
|
2187
|
+
display: flex;
|
|
2188
|
+
flex-direction: column;
|
|
2189
|
+
gap: 0.375rem;
|
|
2190
|
+
}
|
|
2191
|
+
.ns-starter-item {
|
|
2192
|
+
display: block;
|
|
2193
|
+
width: 100%;
|
|
2194
|
+
text-align: left;
|
|
2195
|
+
padding: 0.5rem 0.75rem;
|
|
2196
|
+
border: 1px solid var(--color-border-primary);
|
|
2197
|
+
border-radius: 0.5rem;
|
|
2198
|
+
background: var(--color-bg-primary);
|
|
2199
|
+
color: var(--color-text-secondary);
|
|
2200
|
+
font-size: 0.8125rem;
|
|
2201
|
+
line-height: 1.45;
|
|
2202
|
+
cursor: pointer;
|
|
2203
|
+
transition: border-color 0.14s, background-color 0.14s, color 0.14s;
|
|
2204
|
+
}
|
|
2205
|
+
.ns-starter-item:hover {
|
|
2206
|
+
border-color: color-mix(in srgb, var(--color-accent-primary) 40%, var(--color-border-primary));
|
|
2207
|
+
background: var(--color-bg-hover);
|
|
2208
|
+
color: var(--color-text-primary);
|
|
2209
|
+
}
|
|
2176
2210
|
.ns-panels-label {
|
|
2177
2211
|
width: 100%;
|
|
2178
2212
|
font-size: 0.75rem;
|
|
@@ -2637,7 +2671,6 @@ body {
|
|
|
2637
2671
|
.session-banner { flex-shrink: 0; }
|
|
2638
2672
|
.session-composer {
|
|
2639
2673
|
flex-shrink: 0;
|
|
2640
|
-
margin-bottom: -9px;
|
|
2641
2674
|
display: flex;
|
|
2642
2675
|
padding-inline: 24px;
|
|
2643
2676
|
}
|
|
@@ -5343,7 +5376,7 @@ body {
|
|
|
5343
5376
|
}
|
|
5344
5377
|
.field-input-row .field-input { flex: 1; }
|
|
5345
5378
|
.btn-toggle-key {
|
|
5346
|
-
background: var(--color-
|
|
5379
|
+
background: var(--color-bg-primary);
|
|
5347
5380
|
border: 1px solid var(--color-border-primary);
|
|
5348
5381
|
border-radius: 6px;
|
|
5349
5382
|
color: var(--color-text-secondary);
|
|
@@ -5351,8 +5384,19 @@ body {
|
|
|
5351
5384
|
padding: 0.25rem 0.5rem;
|
|
5352
5385
|
cursor: pointer;
|
|
5353
5386
|
flex-shrink: 0;
|
|
5387
|
+
display: flex;
|
|
5388
|
+
align-items: center;
|
|
5389
|
+
justify-content: center;
|
|
5390
|
+
}
|
|
5391
|
+
.btn-toggle-key:hover {
|
|
5392
|
+
color: var(--color-text-primary);
|
|
5393
|
+
background: var(--color-bg-hover);
|
|
5394
|
+
}
|
|
5395
|
+
.btn-toggle-key:active {
|
|
5396
|
+
background: var(--color-bg-hover);
|
|
5397
|
+
filter: brightness(0.94);
|
|
5398
|
+
transform: scale(0.95);
|
|
5354
5399
|
}
|
|
5355
|
-
.btn-toggle-key:hover { color: var(--color-text-primary); }
|
|
5356
5400
|
.field-checkbox {
|
|
5357
5401
|
width: 1rem;
|
|
5358
5402
|
height: 1rem;
|
|
@@ -5745,14 +5789,15 @@ body {
|
|
|
5745
5789
|
max-height: 90vh;
|
|
5746
5790
|
display: flex;
|
|
5747
5791
|
flex-direction: column;
|
|
5748
|
-
overflow:
|
|
5792
|
+
/* Do NOT use overflow:hidden here — it clips absolutely-positioned dropdowns
|
|
5793
|
+
inside the modal (e.g. the provider custom-select). Border-radius still
|
|
5794
|
+
applies because there is no overflow to clip. */
|
|
5795
|
+
overflow: visible;
|
|
5749
5796
|
}
|
|
5750
5797
|
.model-edit-modal-content {
|
|
5751
5798
|
max-width: 500px;
|
|
5752
5799
|
}
|
|
5753
|
-
|
|
5754
|
-
overflow-y: auto;
|
|
5755
|
-
}
|
|
5800
|
+
/* overflow override for model-edit modal is applied after .modal-content .modal-body below */
|
|
5756
5801
|
.modal-content .modal-header {
|
|
5757
5802
|
display: flex;
|
|
5758
5803
|
align-items: center;
|
|
@@ -5790,6 +5835,12 @@ body {
|
|
|
5790
5835
|
gap: 0.875rem;
|
|
5791
5836
|
padding: 1.25rem;
|
|
5792
5837
|
}
|
|
5838
|
+
/* model-edit modal: allow absolutely-positioned dropdowns to escape the body.
|
|
5839
|
+
Must come after .modal-content .modal-body to win the cascade. */
|
|
5840
|
+
.modal-content.model-edit-modal-content .modal-body {
|
|
5841
|
+
overflow: visible;
|
|
5842
|
+
padding-bottom: 0;
|
|
5843
|
+
}
|
|
5793
5844
|
.modal-content .modal-footer {
|
|
5794
5845
|
display: flex;
|
|
5795
5846
|
align-items: center;
|
|
@@ -6173,6 +6224,9 @@ body {
|
|
|
6173
6224
|
font-size: 0.75rem;
|
|
6174
6225
|
flex: 1;
|
|
6175
6226
|
}
|
|
6227
|
+
.model-test-result:empty {
|
|
6228
|
+
display: none;
|
|
6229
|
+
}
|
|
6176
6230
|
.result-ok { color: var(--color-success); }
|
|
6177
6231
|
.result-fail { color: var(--color-error); }
|
|
6178
6232
|
.result-testing { color: var(--color-text-secondary); }
|
|
@@ -8137,6 +8191,127 @@ body {
|
|
|
8137
8191
|
border-radius: 4px;
|
|
8138
8192
|
}
|
|
8139
8193
|
|
|
8194
|
+
/* ── Markdown split-pane layout ─────────────────────────────────────────── */
|
|
8195
|
+
.code-editor-modal--md {
|
|
8196
|
+
max-width: 1280px;
|
|
8197
|
+
width: 98%;
|
|
8198
|
+
height: 90vh;
|
|
8199
|
+
max-height: 960px;
|
|
8200
|
+
}
|
|
8201
|
+
.code-editor-body--split {
|
|
8202
|
+
display: flex;
|
|
8203
|
+
flex-direction: row;
|
|
8204
|
+
overflow: hidden;
|
|
8205
|
+
}
|
|
8206
|
+
.code-editor-split-left {
|
|
8207
|
+
flex: 1;
|
|
8208
|
+
min-width: 0;
|
|
8209
|
+
overflow: hidden;
|
|
8210
|
+
border-right: 1px solid var(--color-border-primary);
|
|
8211
|
+
display: flex;
|
|
8212
|
+
flex-direction: column;
|
|
8213
|
+
}
|
|
8214
|
+
.code-editor-split-left .cm-editor {
|
|
8215
|
+
height: 100%;
|
|
8216
|
+
flex: 1;
|
|
8217
|
+
}
|
|
8218
|
+
.code-editor-split-left > .cm-editor {
|
|
8219
|
+
height: 100%;
|
|
8220
|
+
}
|
|
8221
|
+
.code-editor-split-divider {
|
|
8222
|
+
display: none;
|
|
8223
|
+
}
|
|
8224
|
+
.code-editor-split-right {
|
|
8225
|
+
flex: 1;
|
|
8226
|
+
min-width: 0;
|
|
8227
|
+
display: flex;
|
|
8228
|
+
flex-direction: column;
|
|
8229
|
+
overflow: hidden;
|
|
8230
|
+
}
|
|
8231
|
+
.code-editor-markdown-preview {
|
|
8232
|
+
flex: 1;
|
|
8233
|
+
overflow-y: auto;
|
|
8234
|
+
padding: 0 1.25rem;
|
|
8235
|
+
font-size: 0.875rem;
|
|
8236
|
+
line-height: 1.7;
|
|
8237
|
+
color: var(--color-text-primary);
|
|
8238
|
+
}
|
|
8239
|
+
.code-editor-markdown-preview h1,
|
|
8240
|
+
.code-editor-markdown-preview h2,
|
|
8241
|
+
.code-editor-markdown-preview h3,
|
|
8242
|
+
.code-editor-markdown-preview h4 {
|
|
8243
|
+
margin-top: 1.25em;
|
|
8244
|
+
margin-bottom: 0.5em;
|
|
8245
|
+
font-weight: 600;
|
|
8246
|
+
}
|
|
8247
|
+
.code-editor-markdown-preview h1:first-child,
|
|
8248
|
+
.code-editor-markdown-preview h2:first-child,
|
|
8249
|
+
.code-editor-markdown-preview h3:first-child { margin-top: 0; }
|
|
8250
|
+
.code-editor-markdown-preview h1 { font-size: 1.375rem; }
|
|
8251
|
+
.code-editor-markdown-preview h2 { font-size: 1.125rem; border-bottom: 1px solid var(--color-border); padding-bottom: 0.25em; }
|
|
8252
|
+
.code-editor-markdown-preview h3 { font-size: 1rem; }
|
|
8253
|
+
.code-editor-markdown-preview p { margin: 0.5em 0; }
|
|
8254
|
+
.code-editor-markdown-preview ul,
|
|
8255
|
+
.code-editor-markdown-preview ol { padding-left: 1.5em; margin: 0.5em 0; }
|
|
8256
|
+
.code-editor-markdown-preview li { margin: 0.2em 0; }
|
|
8257
|
+
.code-editor-markdown-preview code {
|
|
8258
|
+
font-family: var(--font-mono, 'SF Mono', 'Fira Code', monospace);
|
|
8259
|
+
font-size: 0.8125em;
|
|
8260
|
+
background: rgba(128, 128, 128, 0.15);
|
|
8261
|
+
border-radius: 4px;
|
|
8262
|
+
padding: 0.15em 0.4em;
|
|
8263
|
+
}
|
|
8264
|
+
.code-editor-markdown-preview pre {
|
|
8265
|
+
background: rgba(128, 128, 128, 0.15);
|
|
8266
|
+
border-radius: 8px;
|
|
8267
|
+
padding: 0.875em;
|
|
8268
|
+
overflow-x: auto;
|
|
8269
|
+
margin: 0.625em 0;
|
|
8270
|
+
}
|
|
8271
|
+
.code-editor-markdown-preview pre code {
|
|
8272
|
+
background: none;
|
|
8273
|
+
padding: 0;
|
|
8274
|
+
font-size: 0.8125rem;
|
|
8275
|
+
}
|
|
8276
|
+
.code-editor-markdown-preview blockquote {
|
|
8277
|
+
border-left: 3px solid var(--color-accent-primary, #7c3aed);
|
|
8278
|
+
margin: 0.75em 0;
|
|
8279
|
+
padding: 0.25em 1em;
|
|
8280
|
+
color: var(--color-text-secondary);
|
|
8281
|
+
}
|
|
8282
|
+
.code-editor-markdown-preview a {
|
|
8283
|
+
color: var(--color-accent-primary);
|
|
8284
|
+
text-decoration: none;
|
|
8285
|
+
}
|
|
8286
|
+
.code-editor-markdown-preview a:hover { text-decoration: underline; }
|
|
8287
|
+
.code-editor-markdown-preview img {
|
|
8288
|
+
display: inline;
|
|
8289
|
+
vertical-align: middle;
|
|
8290
|
+
max-width: 100%;
|
|
8291
|
+
height: auto;
|
|
8292
|
+
}
|
|
8293
|
+
.code-editor-markdown-preview hr {
|
|
8294
|
+
border: none;
|
|
8295
|
+
border-top: 1px solid var(--color-border-primary);
|
|
8296
|
+
margin: 1.25em 0;
|
|
8297
|
+
}
|
|
8298
|
+
.code-editor-markdown-preview table {
|
|
8299
|
+
border-collapse: collapse;
|
|
8300
|
+
width: 100%;
|
|
8301
|
+
margin: 0.75em 0;
|
|
8302
|
+
font-size: 0.8125rem;
|
|
8303
|
+
}
|
|
8304
|
+
.code-editor-markdown-preview th,
|
|
8305
|
+
.code-editor-markdown-preview td {
|
|
8306
|
+
border: 1px solid var(--color-border-primary);
|
|
8307
|
+
padding: 0.375em 0.75em;
|
|
8308
|
+
text-align: left;
|
|
8309
|
+
}
|
|
8310
|
+
.code-editor-markdown-preview th {
|
|
8311
|
+
background: rgba(128, 128, 128, 0.15);
|
|
8312
|
+
font-weight: 600;
|
|
8313
|
+
}
|
|
8314
|
+
|
|
8140
8315
|
/* ── Skills sidebar section ──────────────────────────────────────────────── */
|
|
8141
8316
|
#skill-list-items { padding: 0 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 2px; }
|
|
8142
8317
|
|
|
@@ -8967,10 +9142,11 @@ body.setup-mode[data-theme="dark"] {
|
|
|
8967
9142
|
padding-top: 0.75rem;
|
|
8968
9143
|
border-top: 1px solid var(--color-border-primary);
|
|
8969
9144
|
}
|
|
8970
|
-
.badge-active
|
|
8971
|
-
.badge-expired
|
|
8972
|
-
.badge-expiring
|
|
8973
|
-
.badge-
|
|
9145
|
+
.badge-active { color: var(--color-success); }
|
|
9146
|
+
.badge-expired { color: var(--color-error); }
|
|
9147
|
+
.badge-expiring { color: var(--color-warning); }
|
|
9148
|
+
.badge-unreachable { color: var(--color-text-secondary); }
|
|
9149
|
+
.badge-inactive { color: var(--color-text-secondary); }
|
|
8974
9150
|
|
|
8975
9151
|
/* Support QR code */
|
|
8976
9152
|
.brand-support-contact {
|
|
@@ -10908,6 +11084,19 @@ body.setup-mode[data-theme="dark"] {
|
|
|
10908
11084
|
flex-wrap: wrap;
|
|
10909
11085
|
}
|
|
10910
11086
|
|
|
11087
|
+
/* ── Disclaimer notice ──────────────────────────────────────────────── */
|
|
11088
|
+
.billing-disclaimer {
|
|
11089
|
+
display: flex;
|
|
11090
|
+
align-items: center;
|
|
11091
|
+
gap: 0.4rem;
|
|
11092
|
+
font-size: 0.75rem;
|
|
11093
|
+
color: var(--color-text-tertiary);
|
|
11094
|
+
padding: 0.4rem 0.75rem;
|
|
11095
|
+
background: var(--color-bg-secondary);
|
|
11096
|
+
border: 1px solid var(--color-border-primary);
|
|
11097
|
+
border-radius: 6px;
|
|
11098
|
+
}
|
|
11099
|
+
|
|
10911
11100
|
/* ── Period Button Group ─────────────────────────────────────────────── */
|
|
10912
11101
|
.billing-period-group {
|
|
10913
11102
|
display: flex;
|
|
@@ -11008,7 +11197,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11008
11197
|
.billing-share-btn:hover {
|
|
11009
11198
|
background: var(--color-bg-tertiary);
|
|
11010
11199
|
color: var(--color-text-primary);
|
|
11011
|
-
border-color: var(--color-
|
|
11200
|
+
border-color: var(--color-accent-primary);
|
|
11012
11201
|
}
|
|
11013
11202
|
.billing-clear-popup {
|
|
11014
11203
|
position: absolute;
|
|
@@ -11084,10 +11273,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11084
11273
|
justify-content: center;
|
|
11085
11274
|
flex-shrink: 0;
|
|
11086
11275
|
}
|
|
11087
|
-
.billing-stat-icon-cost
|
|
11088
|
-
.billing-stat-icon-tokens { background: color-mix(in srgb, #
|
|
11276
|
+
.billing-stat-icon-cost { background: color-mix(in srgb, var(--color-accent-primary) 10%, transparent); color: var(--color-accent-primary); }
|
|
11277
|
+
.billing-stat-icon-tokens { background: color-mix(in srgb, #2075ffbf 10%, transparent); color: #2075ffbf; }
|
|
11089
11278
|
.billing-stat-icon-requests { background: color-mix(in srgb, #f59e0b 10%, transparent); color: #f59e0b; }
|
|
11090
|
-
.billing-stat-icon-cache { background: color-mix(in srgb,
|
|
11279
|
+
.billing-stat-icon-cache { background: color-mix(in srgb, #10b981 10%, transparent); color: #10b981; }
|
|
11091
11280
|
.billing-stat-icon svg {
|
|
11092
11281
|
width: 18px;
|
|
11093
11282
|
height: 18px;
|
|
@@ -11318,7 +11507,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11318
11507
|
align-items: stretch;
|
|
11319
11508
|
}
|
|
11320
11509
|
.billing-cache-hit {
|
|
11321
|
-
background: #
|
|
11510
|
+
background: #2075ffbf;
|
|
11322
11511
|
width: 100%;
|
|
11323
11512
|
border-radius: 2px 2px 0 0;
|
|
11324
11513
|
transition: height 0.2s ease;
|
|
@@ -11346,7 +11535,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11346
11535
|
|
|
11347
11536
|
/* Legend colors — indigo + emerald */
|
|
11348
11537
|
.billing-legend-total { background: var(--color-accent-primary); }
|
|
11349
|
-
.billing-legend-cache-hit { background: #
|
|
11538
|
+
.billing-legend-cache-hit { background: #2075ffbf; }
|
|
11350
11539
|
.billing-legend-cache-miss{ background: #f59e0b; }
|
|
11351
11540
|
.billing-legend-output { background: #34d399; }
|
|
11352
11541
|
|
|
@@ -11397,7 +11586,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11397
11586
|
flex-shrink: 0;
|
|
11398
11587
|
}
|
|
11399
11588
|
.tooltip-total { background: var(--color-accent-primary); }
|
|
11400
|
-
.tooltip-cache-hit { background: #
|
|
11589
|
+
.tooltip-cache-hit { background: #2075ffbf; }
|
|
11401
11590
|
.tooltip-cache-miss{ background: #f59e0b; }
|
|
11402
11591
|
.tooltip-output { background: #34d399; }.tooltip-label {
|
|
11403
11592
|
flex: 1;
|
|
@@ -11472,7 +11661,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11472
11661
|
background: #34d399;
|
|
11473
11662
|
}
|
|
11474
11663
|
.billing-bar-cache-read {
|
|
11475
|
-
background: #
|
|
11664
|
+
background: #2075ffbf;
|
|
11476
11665
|
}
|
|
11477
11666
|
.billing-bar-cache-miss {
|
|
11478
11667
|
background: #f59e0b;
|
|
@@ -11951,7 +12140,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11951
12140
|
}
|
|
11952
12141
|
|
|
11953
12142
|
.swatch-indigo { background: #4f46e5; }
|
|
11954
|
-
.swatch-aurora-blue { background: #
|
|
12143
|
+
.swatch-aurora-blue { background: #2075ffbf; }
|
|
11955
12144
|
.swatch-forest-green { background: #10B981; }
|
|
11956
12145
|
.swatch-sunrise-orange { background: #F59E0B; }
|
|
11957
12146
|
.swatch-rose-violet { background: #8B5CF6; }
|
|
@@ -12172,7 +12361,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
12172
12361
|
color: var(--color-accent-primary);
|
|
12173
12362
|
}
|
|
12174
12363
|
.billing-cell-hit {
|
|
12175
|
-
color: #
|
|
12364
|
+
color: #2075ffbf;
|
|
12176
12365
|
}
|
|
12177
12366
|
.billing-cell-miss {
|
|
12178
12367
|
color: #f59e0b;
|
data/lib/clacky/web/app.js
CHANGED
|
@@ -97,7 +97,11 @@ const Router = (() => {
|
|
|
97
97
|
if (h === "mcp") return { view: "mcp", params: {} };
|
|
98
98
|
if (h === "extensions") return { view: "extensions", params: {} };
|
|
99
99
|
const mExtDetail = h.match(/^extensions\/(.+)$/);
|
|
100
|
-
if (mExtDetail)
|
|
100
|
+
if (mExtDetail) {
|
|
101
|
+
const [detailId, qs] = mExtDetail[1].split("?");
|
|
102
|
+
const source = new URLSearchParams(qs || "").get("source") || null;
|
|
103
|
+
return { view: "extensions", params: { detailId, source } };
|
|
104
|
+
}
|
|
101
105
|
if (h === "trash") return { view: "trash", params: {} };
|
|
102
106
|
if (h === "profile") return { view: "profile", params: {} };
|
|
103
107
|
// Legacy: #memories redirects to #profile (memories are now merged into
|
|
@@ -256,8 +260,9 @@ const Router = (() => {
|
|
|
256
260
|
case "extensions":
|
|
257
261
|
$("extensions-panel").style.display = "flex";
|
|
258
262
|
if (params.detailId) {
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
const sourceQs = params.source ? `?source=${encodeURIComponent(params.source)}` : "";
|
|
264
|
+
_setHash(`extensions/${params.detailId}${sourceQs}`);
|
|
265
|
+
Extensions.onPanelShow({ detailId: params.detailId, source: params.source });
|
|
261
266
|
} else {
|
|
262
267
|
_setHash("extensions");
|
|
263
268
|
Extensions.onPanelShow();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* global CM */
|
|
1
|
+
/* global CM, marked */
|
|
2
2
|
/**
|
|
3
3
|
* CodeEditor — a reusable wrapper around CodeMirror 6.
|
|
4
4
|
*
|
|
@@ -31,16 +31,20 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function _detectLanguage(filename) {
|
|
34
|
-
if (!filename) return
|
|
34
|
+
if (!filename) return null;
|
|
35
35
|
const ext = filename.split(".").pop().toLowerCase();
|
|
36
36
|
const map = { md: "markdown", markdown: "markdown" };
|
|
37
|
-
return map[ext] ||
|
|
37
|
+
return map[ext] || null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function _isDark() {
|
|
41
41
|
return document.documentElement.getAttribute("data-theme") === "dark";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function _isMarkdown(language) {
|
|
45
|
+
return language === "markdown" || language === "md";
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
function _buildExtensions(opts) {
|
|
45
49
|
const extensions = [
|
|
46
50
|
CM.lineNumbers(),
|
|
@@ -86,9 +90,24 @@
|
|
|
86
90
|
}]));
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
if (opts.onChange) {
|
|
94
|
+
extensions.push(CM.EditorView.updateListener.of((update) => {
|
|
95
|
+
if (update.docChanged) opts.onChange(update.state.doc.toString());
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
return extensions;
|
|
90
100
|
}
|
|
91
101
|
|
|
102
|
+
function _renderMarkdownHtml(text) {
|
|
103
|
+
if (typeof marked !== "undefined") {
|
|
104
|
+
try {
|
|
105
|
+
return marked.parse(text, { breaks: true, gfm: true });
|
|
106
|
+
} catch (_) { /* fall through */ }
|
|
107
|
+
}
|
|
108
|
+
return `<pre>${text.replace(/</g, "<")}</pre>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
function open(opts) {
|
|
93
112
|
const {
|
|
94
113
|
content = "",
|
|
@@ -101,6 +120,7 @@
|
|
|
101
120
|
|
|
102
121
|
const kind = opts.kind || (opts.filename ? _fileKind(opts.filename) : "text");
|
|
103
122
|
const language = opts.language || _detectLanguage(opts.filename);
|
|
123
|
+
const isMd = _isMarkdown(language) && kind !== "image";
|
|
104
124
|
|
|
105
125
|
let overlay = document.getElementById("code-editor-overlay");
|
|
106
126
|
if (overlay) overlay.remove();
|
|
@@ -110,8 +130,8 @@
|
|
|
110
130
|
overlay.className = "modal-overlay";
|
|
111
131
|
|
|
112
132
|
const cancelLabel = I18n.t("modal.cancel");
|
|
113
|
-
const closeLabel
|
|
114
|
-
const saveLabel
|
|
133
|
+
const closeLabel = I18n.t("modal.close");
|
|
134
|
+
const saveLabel = I18n.t("modal.save");
|
|
115
135
|
|
|
116
136
|
const isReadOnlyOrImage = readOnly || kind === "image";
|
|
117
137
|
const footerActions = isReadOnlyOrImage
|
|
@@ -119,12 +139,14 @@
|
|
|
119
139
|
: `<button class="btn btn-secondary code-editor-cancel">${cancelLabel}</button><button class="btn btn-primary code-editor-save">${saveLabel}</button>`;
|
|
120
140
|
|
|
121
141
|
overlay.innerHTML = `
|
|
122
|
-
<div class="code-editor-modal${kind === "image" ? " code-editor-modal--image" : ""}">
|
|
142
|
+
<div class="code-editor-modal${kind === "image" ? " code-editor-modal--image" : ""}${isMd ? " code-editor-modal--md" : ""}">
|
|
123
143
|
<div class="code-editor-header">
|
|
124
144
|
<h3 class="code-editor-title"></h3>
|
|
125
145
|
<button class="code-editor-close" title="${closeLabel}">×</button>
|
|
126
146
|
</div>
|
|
127
|
-
<div class="code-editor-body"
|
|
147
|
+
<div class="code-editor-body${isMd ? " code-editor-body--split" : ""}">
|
|
148
|
+
${isMd ? `<div class="code-editor-split-left"></div><div class="code-editor-split-divider"></div><div class="code-editor-split-right"><div class="code-editor-markdown-preview"></div></div>` : ""}
|
|
149
|
+
</div>
|
|
128
150
|
<div class="code-editor-footer">
|
|
129
151
|
<span class="code-editor-status"></span>
|
|
130
152
|
<div class="code-editor-actions">${footerActions}</div>
|
|
@@ -134,8 +156,8 @@
|
|
|
134
156
|
document.body.appendChild(overlay);
|
|
135
157
|
overlay.querySelector(".code-editor-title").textContent = title;
|
|
136
158
|
|
|
137
|
-
const body
|
|
138
|
-
const status
|
|
159
|
+
const body = overlay.querySelector(".code-editor-body");
|
|
160
|
+
const status = overlay.querySelector(".code-editor-status");
|
|
139
161
|
const closeBtn = overlay.querySelector(".code-editor-close");
|
|
140
162
|
const cancelBtn = overlay.querySelector(".code-editor-cancel");
|
|
141
163
|
const saveBtn = overlay.querySelector(".code-editor-save");
|
|
@@ -147,7 +169,6 @@
|
|
|
147
169
|
|
|
148
170
|
closeBtn.addEventListener("click", close);
|
|
149
171
|
if (cancelBtn) cancelBtn.addEventListener("click", close);
|
|
150
|
-
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
151
172
|
|
|
152
173
|
if (kind === "image") {
|
|
153
174
|
body.classList.add("code-editor-body--image");
|
|
@@ -159,7 +180,21 @@
|
|
|
159
180
|
return { close };
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
|
|
183
|
+
// Editor mount target: left pane for markdown split, body itself otherwise
|
|
184
|
+
const editorParent = isMd ? overlay.querySelector(".code-editor-split-left") : body;
|
|
185
|
+
const previewPane = isMd ? overlay.querySelector(".code-editor-markdown-preview") : null;
|
|
186
|
+
|
|
187
|
+
function updatePreview(text) {
|
|
188
|
+
if (previewPane) previewPane.innerHTML = _renderMarkdownHtml(text);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const editorOpts = {
|
|
192
|
+
language: language || "markdown",
|
|
193
|
+
readOnly,
|
|
194
|
+
onSave: null,
|
|
195
|
+
_getContent: null,
|
|
196
|
+
onChange: isMd ? updatePreview : null,
|
|
197
|
+
};
|
|
163
198
|
const getContent = () => view.state.doc.toString();
|
|
164
199
|
editorOpts._getContent = getContent;
|
|
165
200
|
editorOpts.onSave = onSave ? () => doSave() : null;
|
|
@@ -169,9 +204,12 @@
|
|
|
169
204
|
doc: content,
|
|
170
205
|
extensions: _buildExtensions(editorOpts),
|
|
171
206
|
}),
|
|
172
|
-
parent:
|
|
207
|
+
parent: editorParent,
|
|
173
208
|
});
|
|
174
209
|
|
|
210
|
+
// Render initial preview
|
|
211
|
+
if (isMd) updatePreview(content);
|
|
212
|
+
|
|
175
213
|
async function doSave() {
|
|
176
214
|
if (!onSave) return;
|
|
177
215
|
if (saveBtn) saveBtn.disabled = true;
|
|
@@ -138,7 +138,8 @@ const BillingView = (() => {
|
|
|
138
138
|
<div class="billing-period-group">${periodBtns}</div>
|
|
139
139
|
<select id="billing-model-filter" class="billing-model-filter">${modelOptions}</select>
|
|
140
140
|
<button id="billing-share-btn" class="billing-share-btn" title="${I18n.t('billing.share.tooltip') || 'Share scorecard'}">
|
|
141
|
-
|
|
141
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><polyline points="16 6 12 2 8 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="2" x2="12" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
142
|
+
${I18n.t('billing.share.btn') || 'Share scorecard'}
|
|
142
143
|
</button>
|
|
143
144
|
<div class="billing-clear-container">
|
|
144
145
|
<button id="billing-clear-btn" class="billing-clear-btn" title="${I18n.t('billing.clearData') || 'Clear Data'}">
|
|
@@ -152,6 +153,11 @@ const BillingView = (() => {
|
|
|
152
153
|
</div>
|
|
153
154
|
</div>
|
|
154
155
|
|
|
156
|
+
<div class="billing-disclaimer">
|
|
157
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.4"/><path d="M8 7v4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="5" r="0.7" fill="currentColor"/></svg>
|
|
158
|
+
${I18n.t("billing.disclaimer")}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
155
161
|
<div class="billing-stats-row">
|
|
156
162
|
<div class="billing-stat-card">
|
|
157
163
|
<div class="billing-stat-icon billing-stat-icon-cost">
|
|
@@ -158,7 +158,11 @@ const ExtensionsStore = (() => {
|
|
|
158
158
|
try {
|
|
159
159
|
const res = await fetch("/api/store/extensions/brand");
|
|
160
160
|
const data = await res.json();
|
|
161
|
-
|
|
161
|
+
const all = data.extensions || [];
|
|
162
|
+
_extensions = _query
|
|
163
|
+
? all.filter(e => [e.name, e.name_zh, e.description].some(
|
|
164
|
+
f => f && f.toLowerCase().includes(_query.toLowerCase())))
|
|
165
|
+
: all;
|
|
162
166
|
_error = data.warning || null;
|
|
163
167
|
_loading = false;
|
|
164
168
|
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
@@ -174,14 +178,18 @@ const ExtensionsStore = (() => {
|
|
|
174
178
|
},
|
|
175
179
|
|
|
176
180
|
/** Open the detail view for one extension (fetches contributes + versions). */
|
|
177
|
-
async loadDetail(id) {
|
|
181
|
+
async loadDetail(id, source) {
|
|
178
182
|
if (!id) return;
|
|
179
183
|
_detail = null;
|
|
180
184
|
_detailLoading = true;
|
|
181
185
|
_detailError = null;
|
|
182
186
|
_emit("extensions:detail");
|
|
183
187
|
try {
|
|
184
|
-
const
|
|
188
|
+
const isBrand = source === "brand" || _filterBrand;
|
|
189
|
+
const url = isBrand
|
|
190
|
+
? "/api/store/extension?id=" + encodeURIComponent(id) + "&source=brand"
|
|
191
|
+
: "/api/store/extension?id=" + encodeURIComponent(id);
|
|
192
|
+
const res = await fetch(url);
|
|
185
193
|
const data = await res.json();
|
|
186
194
|
if (res.ok && data.ok && data.extension) {
|
|
187
195
|
_detail = data.extension;
|
|
@@ -243,10 +251,18 @@ const ExtensionsStore = (() => {
|
|
|
243
251
|
async install(id) {
|
|
244
252
|
if (!id) return;
|
|
245
253
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
254
|
+
// Prefer the already-loaded detail (avoids a second round-trip and correctly
|
|
255
|
+
// handles brand-private extensions whose detail was fetched with &source=brand).
|
|
256
|
+
let ext;
|
|
257
|
+
if (_detail && String(_detail.id) === String(id)) {
|
|
258
|
+
ext = _detail;
|
|
259
|
+
} else {
|
|
260
|
+
const source = _filterBrand ? "&source=brand" : "";
|
|
261
|
+
const detailRes = await fetch("/api/store/extension?id=" + encodeURIComponent(id) + source);
|
|
262
|
+
const detailData = await detailRes.json();
|
|
263
|
+
if (!detailRes.ok || !detailData.ok) throw new Error(detailData.error || "fetch detail failed");
|
|
264
|
+
ext = detailData.extension;
|
|
265
|
+
}
|
|
250
266
|
const download_url = ext.download_url;
|
|
251
267
|
if (!download_url) throw new Error("No download URL available");
|
|
252
268
|
const res = await fetch("/api/store/extension/install", {
|
|
@@ -121,7 +121,8 @@ const ExtensionsView = (() => {
|
|
|
121
121
|
|
|
122
122
|
const card = document.createElement("div");
|
|
123
123
|
card.className = "extension-card extension-card-clickable";
|
|
124
|
-
card.dataset.extId
|
|
124
|
+
card.dataset.extId = ext.id != null ? String(ext.id) : (ext.name || "");
|
|
125
|
+
card.dataset.extOrigin = ext.origin || "";
|
|
125
126
|
card.innerHTML = `
|
|
126
127
|
<div class="extension-card-main">
|
|
127
128
|
${emojiHtml}
|
|
@@ -474,8 +475,11 @@ const ExtensionsView = (() => {
|
|
|
474
475
|
const card = e.target.closest(".extension-card-clickable");
|
|
475
476
|
if (card && card.dataset.extId) {
|
|
476
477
|
const router = window.Clacky && window.Clacky.Router;
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
// Use origin stored on the card; origin="self" means brand-private extension.
|
|
479
|
+
const isBrand = card.dataset.extOrigin === "self" || Extensions.state.filterBrand;
|
|
480
|
+
const source = isBrand ? "brand" : null;
|
|
481
|
+
if (router) router.navigate("extensions", { detailId: card.dataset.extId, source });
|
|
482
|
+
else Extensions.loadDetail(card.dataset.extId, source);
|
|
479
483
|
}
|
|
480
484
|
});
|
|
481
485
|
}
|
|
@@ -506,7 +510,7 @@ const ExtensionsView = (() => {
|
|
|
506
510
|
const detailId = opts && opts.detailId;
|
|
507
511
|
if (detailId) {
|
|
508
512
|
Extensions.load();
|
|
509
|
-
Extensions.loadDetail(detailId);
|
|
513
|
+
Extensions.loadDetail(detailId, opts && opts.source);
|
|
510
514
|
} else {
|
|
511
515
|
Extensions.closeDetail();
|
|
512
516
|
Extensions.load();
|