openclacky 1.2.8 → 1.2.10

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/lib/clacky/agent/llm_caller.rb +3 -0
  4. data/lib/clacky/agent/message_compressor_helper.rb +6 -5
  5. data/lib/clacky/agent/session_serializer.rb +4 -0
  6. data/lib/clacky/agent.rb +9 -0
  7. data/lib/clacky/agent_config.rb +111 -8
  8. data/lib/clacky/brand_config.rb +1 -0
  9. data/lib/clacky/cli.rb +49 -22
  10. data/lib/clacky/client.rb +6 -2
  11. data/lib/clacky/default_skills/channel-manager/SKILL.md +33 -110
  12. data/lib/clacky/default_skills/media-gen/SKILL.md +128 -0
  13. data/lib/clacky/idle_compression_timer.rb +38 -15
  14. data/lib/clacky/media/base.rb +68 -0
  15. data/lib/clacky/media/gemini.rb +36 -0
  16. data/lib/clacky/media/generator.rb +78 -0
  17. data/lib/clacky/media/openai_compat.rb +168 -0
  18. data/lib/clacky/providers.rb +89 -2
  19. data/lib/clacky/rich_ui_controller.rb +1549 -0
  20. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +24 -2
  21. data/lib/clacky/server/channel/channel_manager.rb +89 -2
  22. data/lib/clacky/server/http_server.rb +334 -29
  23. data/lib/clacky/session_manager.rb +9 -8
  24. data/lib/clacky/telemetry.rb +26 -6
  25. data/lib/clacky/ui2/layout_manager.rb +11 -7
  26. data/lib/clacky/ui2/ui_controller.rb +2 -2
  27. data/lib/clacky/ui_interface.rb +1 -1
  28. data/lib/clacky/utils/model_pricing.rb +75 -53
  29. data/lib/clacky/version.rb +1 -1
  30. data/lib/clacky/web/app.css +393 -14
  31. data/lib/clacky/web/billing.js +1 -1
  32. data/lib/clacky/web/i18n.js +86 -4
  33. data/lib/clacky/web/index.html +23 -3
  34. data/lib/clacky/web/model-tester.js +58 -0
  35. data/lib/clacky/web/onboard.js +17 -30
  36. data/lib/clacky/web/sessions.js +443 -2
  37. data/lib/clacky/web/settings.js +372 -97
  38. data/lib/clacky/web/workspace.js +9 -1
  39. data/lib/clacky.rb +3 -0
  40. data/scripts/build/lib/network.sh +61 -30
  41. data/scripts/install.ps1 +16 -4
  42. data/scripts/install.sh +61 -30
  43. data/scripts/install_browser.sh +61 -30
  44. data/scripts/install_full.sh +61 -30
  45. data/scripts/install_rails_deps.sh +61 -30
  46. data/scripts/install_system_deps.sh +61 -30
  47. metadata +12 -3
  48. data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +0 -574
@@ -3858,6 +3858,7 @@ body {
3858
3858
  .model-card-grid-actions {
3859
3859
  display: flex;
3860
3860
  flex-direction: row;
3861
+ align-items: center;
3861
3862
  flex-wrap: wrap;
3862
3863
  gap: 0.5rem;
3863
3864
  padding-top: 0.625rem;
@@ -3867,7 +3868,7 @@ body {
3867
3868
  display: inline-flex;
3868
3869
  align-items: center;
3869
3870
  justify-content: center;
3870
- gap: 0.25rem;
3871
+ gap: 0.3125rem;
3871
3872
  padding: 0.375rem 0.5rem;
3872
3873
  border: 1px solid var(--color-border-primary);
3873
3874
  border-radius: 6px;
@@ -3877,15 +3878,9 @@ body {
3877
3878
  cursor: pointer;
3878
3879
  transition: all 0.15s;
3879
3880
  white-space: nowrap;
3881
+ min-width: 0;
3880
3882
  }
3881
- .btn-card-grid-action svg {
3882
- flex-shrink: 0;
3883
- }
3884
- .btn-card-grid-action span {
3885
- overflow: hidden;
3886
- text-overflow: ellipsis;
3887
- white-space: nowrap;
3888
- }
3883
+ .btn-card-grid-action svg { flex-shrink: 0; }
3889
3884
  .btn-card-grid-action:hover:not(:disabled) {
3890
3885
  border-color: var(--color-accent-primary);
3891
3886
  color: var(--color-accent-primary);
@@ -3895,13 +3890,22 @@ body {
3895
3890
  opacity: 0.5;
3896
3891
  cursor: not-allowed;
3897
3892
  }
3893
+ .btn-card-grid-action-primary {
3894
+ background: var(--color-bg-secondary);
3895
+ color: var(--color-text-primary);
3896
+ font-weight: 500;
3897
+ }
3898
3898
  .btn-card-grid-action-danger:hover:not(:disabled) {
3899
3899
  border-color: var(--color-error);
3900
3900
  color: var(--color-error);
3901
3901
  background: color-mix(in srgb, var(--color-error) 8%, transparent);
3902
3902
  }
3903
- a.btn-card-grid-action {
3904
- text-decoration: none;
3903
+ .model-card-grid-toolbar {
3904
+ display: flex;
3905
+ flex-wrap: wrap;
3906
+ gap: 0.375rem;
3907
+ margin-left: auto;
3908
+ justify-content: flex-end;
3905
3909
  }
3906
3910
  .model-card-grid-footer {
3907
3911
  display: flex;
@@ -4104,6 +4108,175 @@ a.btn-card-grid-action {
4104
4108
  border-color: var(--color-border-secondary);
4105
4109
  }
4106
4110
 
4111
+ /* ── Directory Picker Modal (tree-based) ─────────────────────────────────── */
4112
+ .modal-title {
4113
+ padding: 1rem 1.25rem;
4114
+ border-bottom: 1px solid var(--color-border-primary);
4115
+ font-size: 1.0625rem;
4116
+ font-weight: 600;
4117
+ color: var(--color-text-primary);
4118
+ }
4119
+ .modal-buttons {
4120
+ display: flex;
4121
+ align-items: center;
4122
+ justify-content: flex-end;
4123
+ gap: 0.625rem;
4124
+ padding: 1rem 1.25rem;
4125
+ border-top: 1px solid var(--color-border-primary);
4126
+ }
4127
+ .dir-picker-input {
4128
+ width: 100%;
4129
+ padding: 0.5rem 0.75rem;
4130
+ border: 1px solid var(--color-border-primary);
4131
+ border-radius: 6px;
4132
+ background: var(--color-bg-primary);
4133
+ color: var(--color-text-primary);
4134
+ font-size: 0.875rem;
4135
+ font-family: var(--font-mono, monospace);
4136
+ transition: border-color 0.15s;
4137
+ box-sizing: border-box;
4138
+ }.dir-picker-input:focus {
4139
+ outline: none;
4140
+ border-color: var(--color-accent);
4141
+ }
4142
+ .dir-picker-input::placeholder {
4143
+ color: var(--color-text-tertiary);
4144
+ }
4145
+ /* Preset buttons row */
4146
+ .dp-presets {
4147
+ display: flex;
4148
+ gap: 0.5rem;
4149
+ flex-wrap: wrap;
4150
+ }
4151
+ .dp-presets .btn-sm {
4152
+ padding: 0.25rem 0.625rem;
4153
+ font-size: 0.75rem;
4154
+ }
4155
+ /* Path input container */
4156
+ .dp-path-container {
4157
+ flex-shrink: 0;
4158
+ position: relative;
4159
+ }/* Autocomplete dropdown */
4160
+ .dp-autocomplete {
4161
+ position: absolute;
4162
+ top: 100%;
4163
+ left: 0;
4164
+ right: 0;
4165
+ z-index: 10;
4166
+ max-height: 200px;
4167
+ overflow-y: auto;
4168
+ background: var(--color-bg-primary);
4169
+ border: 1px solid var(--color-border-primary);
4170
+ border-top: none;
4171
+ border-radius: 0 0 6px 6px;
4172
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
4173
+ }
4174
+ .dp-ac-item {
4175
+ display: flex;
4176
+ align-items: center;
4177
+ gap: 0.5rem;
4178
+ padding: 0.4rem 0.75rem;
4179
+ cursor: pointer;
4180
+ font-size: 0.8125rem;
4181
+ color: var(--color-text-primary);
4182
+ transition: background 0.1s;
4183
+ }
4184
+ .dp-ac-item:hover,
4185
+ .dp-ac-item.active {
4186
+ background: var(--color-bg-hover);
4187
+ }
4188
+ .dp-ac-icon {
4189
+ display: inline-flex;
4190
+ align-items: center;
4191
+ flex-shrink: 0;
4192
+ color: var(--color-text-secondary);
4193
+ }
4194
+ .dp-ac-name {
4195
+ overflow: hidden;
4196
+ text-overflow: ellipsis;
4197
+ white-space: nowrap;
4198
+ }
4199
+ /* Tree container */
4200
+ .dp-tree {
4201
+ border: 1px solid var(--color-border-primary);
4202
+ border-radius: 6px;
4203
+ background: var(--color-bg-secondary);
4204
+ min-height: 200px;
4205
+ max-height: 340px;
4206
+ overflow: auto;
4207
+ font-size: 0.8125rem;
4208
+ }
4209
+ /* Tree node */
4210
+ .dp-node {
4211
+ user-select: none;
4212
+ }
4213
+ .dp-row {
4214
+ display: flex;
4215
+ align-items: center;
4216
+ gap: 0.375rem;
4217
+ padding: 0.375rem 0.5rem;
4218
+ cursor: pointer;
4219
+ border-radius: 4px;
4220
+ transition: background 0.1s;
4221
+ }
4222
+ .dp-row:hover {
4223
+ background: var(--color-bg-hover);
4224
+ }
4225
+ .dp-row.selected {
4226
+ background: var(--color-accent-subtle, rgba(59, 130, 246, 0.12));
4227
+ }
4228
+ .dp-row.selected .dp-name {
4229
+ color: var(--color-accent);
4230
+ font-weight: 500;
4231
+ }
4232
+ /* Caret */
4233
+ .dp-caret {
4234
+ display: inline-flex;
4235
+ align-items: center;
4236
+ justify-content: center;
4237
+ width: 16px;
4238
+ height: 16px;
4239
+ flex-shrink: 0;
4240
+ color: var(--color-text-tertiary);
4241
+ transition: transform 0.15s;
4242
+ }
4243
+ .dp-caret.open {
4244
+ transform: rotate(90deg);
4245
+ }
4246
+ /* Folder icon */
4247
+ .dp-icon {
4248
+ display: inline-flex;
4249
+ align-items: center;
4250
+ flex-shrink: 0;
4251
+ color: var(--color-text-secondary);
4252
+ }
4253
+ .dp-row.selected .dp-icon {
4254
+ color: var(--color-accent);
4255
+ }
4256
+ /* Name */
4257
+ .dp-name {
4258
+ flex: 1;
4259
+ overflow: hidden;
4260
+ text-overflow: ellipsis;
4261
+ white-space: nowrap;
4262
+ color: var(--color-text-primary);
4263
+ }
4264
+ /* Children container */
4265
+ .dp-children {
4266
+ display: flex;
4267
+ flex-direction: column;
4268
+ }/* Loading / empty / error states */
4269
+ .dp-loading,
4270
+ .dp-empty,
4271
+ .dp-error {
4272
+ padding: 0.5rem 1rem;
4273
+ color: var(--color-text-tertiary);
4274
+ font-size: 0.75rem;
4275
+ font-style: italic;
4276
+ }
4277
+ .dp-error {
4278
+ color: var(--color-text-danger, #ef4444);
4279
+ }
4107
4280
  /* Model name combobox */
4108
4281
  .model-name-combobox {
4109
4282
  position: relative;
@@ -9065,11 +9238,14 @@ body.setup-mode[data-theme="dark"] {
9065
9238
  .tooltip-date {
9066
9239
  font-weight: 600;
9067
9240
  color: var(--color-text-primary);
9241
+ white-space: nowrap;
9068
9242
  }
9069
- .tooltip-total {
9243
+ .tooltip-total-value {
9070
9244
  font-size: 0.875rem;
9071
9245
  color: var(--color-accent);
9072
- font-weight: 500;
9246
+ font-weight: 600;
9247
+ white-space: nowrap;
9248
+ margin-left: 0.75rem;
9073
9249
  }
9074
9250
  .tooltip-row {
9075
9251
  display: flex;
@@ -9567,6 +9743,53 @@ body.setup-mode[data-theme="dark"] {
9567
9743
  color: var(--color-text-secondary);
9568
9744
  font-weight: 500;
9569
9745
  }
9746
+ .settings-exchange-rate-refresh {
9747
+ display: inline-flex;
9748
+ align-items: center;
9749
+ justify-content: center;
9750
+ gap: 0.35rem;
9751
+ min-height: 34px;
9752
+ padding: 0.45rem 0.8rem;
9753
+ border: 1px solid color-mix(in srgb, var(--color-accent-primary) 28%, var(--color-border));
9754
+ border-radius: 999px;
9755
+ background: color-mix(in srgb, var(--color-accent-primary) 8%, var(--color-bg-secondary));
9756
+ color: var(--color-accent-primary);
9757
+ font-size: 0.8125rem;
9758
+ font-weight: 600;
9759
+ line-height: 1;
9760
+ white-space: nowrap;
9761
+ cursor: pointer;
9762
+ transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
9763
+ }
9764
+ .settings-exchange-rate-refresh svg {
9765
+ flex: 0 0 auto;
9766
+ }
9767
+ .settings-exchange-rate-refresh:hover:not(:disabled) {
9768
+ border-color: color-mix(in srgb, var(--color-accent-primary) 52%, var(--color-border));
9769
+ background: color-mix(in srgb, var(--color-accent-primary) 12%, var(--color-bg-secondary));
9770
+ box-shadow: 0 4px 12px color-mix(in srgb, var(--color-accent-primary) 16%, transparent);
9771
+ transform: translateY(-1px);
9772
+ }
9773
+ .settings-exchange-rate-refresh:active:not(:disabled) {
9774
+ transform: translateY(0);
9775
+ }
9776
+ .settings-exchange-rate-refresh:disabled {
9777
+ opacity: 0.6;
9778
+ cursor: not-allowed;
9779
+ transform: none;
9780
+ }
9781
+ .settings-exchange-rate-status {
9782
+ min-height: 1.1rem;
9783
+ margin-top: 0.4rem;
9784
+ font-size: 0.8125rem;
9785
+ color: var(--color-text-secondary);
9786
+ }
9787
+ .settings-exchange-rate-status.success {
9788
+ color: var(--color-success, #2e7d32);
9789
+ }
9790
+ .settings-exchange-rate-status.error {
9791
+ color: var(--color-error, #d33);
9792
+ }
9570
9793
 
9571
9794
  /* ── Sessions List ───────────────────────────────────────────────────── */
9572
9795
  .billing-sessions-row {
@@ -9710,4 +9933,160 @@ body.setup-mode[data-theme="dark"] {
9710
9933
  grid-column: 1 / -1;
9711
9934
  text-align: right;
9712
9935
  }
9713
- }
9936
+ }
9937
+
9938
+ /* ════ Media generation (Settings → Models tab) ════ */
9939
+ .settings-section-desc {
9940
+ font-size: 0.8125rem;
9941
+ color: var(--color-text-secondary);
9942
+ line-height: 1.5;
9943
+ margin: -0.25rem 0 0.5rem;
9944
+ }
9945
+ .media-row {
9946
+ border: 1px solid var(--color-border-primary);
9947
+ border-radius: 8px;
9948
+ background: var(--color-bg-secondary);
9949
+ margin-bottom: 0.5rem;
9950
+ overflow: hidden;
9951
+ }
9952
+ .media-row.is-expanded {
9953
+ background: var(--color-bg-secondary);
9954
+ }
9955
+ .media-row-head {
9956
+ display: flex;
9957
+ align-items: center;
9958
+ gap: 0.75rem;
9959
+ padding: 0.5rem 0.75rem;
9960
+ min-height: 2.25rem;
9961
+ }
9962
+ .media-row-title {
9963
+ font-size: 0.875rem;
9964
+ font-weight: 600;
9965
+ color: var(--color-text-primary);
9966
+ min-width: 3rem;
9967
+ }
9968
+ .media-row-segmented {
9969
+ display: inline-flex;
9970
+ background: var(--color-bg-primary);
9971
+ border: 1px solid var(--color-border-primary);
9972
+ border-radius: 6px;
9973
+ padding: 1px;
9974
+ }
9975
+ .media-row-segmented button {
9976
+ background: transparent;
9977
+ border: none;
9978
+ padding: 0.25rem 0.625rem;
9979
+ font-size: 0.75rem;
9980
+ color: var(--color-text-secondary);
9981
+ border-radius: 5px;
9982
+ cursor: pointer;
9983
+ transition: background-color 0.15s ease, color 0.15s ease;
9984
+ font-family: inherit;
9985
+ }
9986
+ .media-row-segmented button:hover:not(.is-active):not(:disabled) {
9987
+ color: var(--color-text-primary);
9988
+ }
9989
+ .media-row-segmented button.is-active {
9990
+ background: var(--color-button-primary);
9991
+ color: #fff;
9992
+ }
9993
+ .media-row-segmented button:disabled {
9994
+ opacity: 0.4;
9995
+ cursor: not-allowed;
9996
+ }
9997
+ .media-row-status {
9998
+ margin-left: auto;
9999
+ font-size: 0.75rem;
10000
+ color: var(--color-text-secondary);
10001
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
10002
+ white-space: nowrap;
10003
+ overflow: hidden;
10004
+ text-overflow: ellipsis;
10005
+ max-width: 16rem;
10006
+ }
10007
+ .media-row-detail {
10008
+ border-top: 1px solid var(--color-border-primary);
10009
+ padding: 0.625rem 0.75rem;
10010
+ font-size: 0.8125rem;
10011
+ color: var(--color-text-secondary);
10012
+ line-height: 1.5;
10013
+ background: var(--color-bg-primary);
10014
+ }
10015
+ .media-row-detail.is-warning {
10016
+ color: var(--color-warning, #d97706);
10017
+ }
10018
+ .media-row-hint {
10019
+ margin-top: 0.375rem;
10020
+ font-size: 0.75rem;
10021
+ color: var(--color-text-tertiary, var(--color-text-secondary));
10022
+ }
10023
+ .media-row-detail .media-kv {
10024
+ display: grid;
10025
+ grid-template-columns: max-content 1fr;
10026
+ gap: 0.25rem 0.75rem;
10027
+ font-size: 0.8125rem;
10028
+ }
10029
+ .media-row-detail .media-kv-key {
10030
+ color: var(--color-text-secondary);
10031
+ }
10032
+ .media-row-detail .media-kv-val {
10033
+ color: var(--color-text-primary);
10034
+ font-weight: 500;
10035
+ word-break: break-all;
10036
+ }
10037
+ .media-row-actions {
10038
+ display: flex;
10039
+ gap: 0.5rem;
10040
+ margin-top: 0.5rem;
10041
+ justify-content: flex-end;
10042
+ }
10043
+ .media-row-btn {
10044
+ background: transparent;
10045
+ border: 1px solid var(--color-border-primary);
10046
+ color: var(--color-text-primary);
10047
+ border-radius: 6px;
10048
+ padding: 0.3125rem 0.75rem;
10049
+ font-size: 0.75rem;
10050
+ cursor: pointer;
10051
+ transition: background-color 0.15s ease;
10052
+ font-family: inherit;
10053
+ }
10054
+ .media-row-btn:hover {
10055
+ background: var(--color-bg-secondary);
10056
+ }
10057
+ .media-row-btn.is-primary {
10058
+ background: var(--color-button-primary);
10059
+ color: #fff;
10060
+ border-color: transparent;
10061
+ }
10062
+ .media-row-btn.is-primary:hover {
10063
+ background: var(--color-button-primary-hover);
10064
+ }
10065
+ .media-custom-form {
10066
+ display: grid;
10067
+ grid-template-columns: max-content 1fr;
10068
+ gap: 0.4375rem 0.625rem;
10069
+ align-items: center;
10070
+ }
10071
+ .media-custom-form label {
10072
+ font-size: 0.75rem;
10073
+ color: var(--color-text-secondary);
10074
+ }
10075
+ .media-custom-form input {
10076
+ width: 100%;
10077
+ background: var(--color-bg-secondary);
10078
+ border: 1px solid var(--color-border-primary);
10079
+ color: var(--color-text-primary);
10080
+ border-radius: 6px;
10081
+ padding: 0.375rem 0.5rem;
10082
+ font-size: 0.8125rem;
10083
+ font-family: inherit;
10084
+ box-sizing: border-box;
10085
+ }
10086
+ .media-custom-form .media-form-actions {
10087
+ grid-column: 1 / -1;
10088
+ display: flex;
10089
+ gap: 0.5rem;
10090
+ justify-content: flex-end;
10091
+ margin-top: 0.25rem;
10092
+ }
@@ -226,7 +226,7 @@ const Billing = (() => {
226
226
  tooltip.innerHTML = `
227
227
  <div class="tooltip-header">
228
228
  <span class="tooltip-date">${date}</span>
229
- <span class="tooltip-total">${total} tokens</span>
229
+ <span class="tooltip-total-value">${total} tokens</span>
230
230
  </div>
231
231
  <div class="tooltip-row">
232
232
  <span class="tooltip-dot tooltip-total"></span>
@@ -80,8 +80,16 @@ const I18n = (() => {
80
80
  "sessions.actions.download": "Download session files",
81
81
  "sessions.actions.downloadHint": "for debugging",
82
82
  "sib.dir.tooltip": "Click to change directory",
83
- "sib.dir.changePrompt": "Change working directory:",
84
- "workspace.title": "Workspace",
83
+ "sib.dir.changePrompt": "Change working directory",
84
+ "sib.dir.home": "Home",
85
+ "sib.dir.root": "Root",
86
+ "sib.dir.default": "Default workspace",
87
+ "sib.dir.current": "Current workspace", "sib.dir.inputPlaceholder": "Type or select a directory path",
88
+ "sib.dir.loading": "Loading…",
89
+ "sib.dir.empty": "Empty directory",
90
+ "sib.dir.loadError": "Failed to load",
91
+ "sib.dir.confirm": "Confirm",
92
+ "sib.dir.cancel": "Cancel", "workspace.title": "Workspace",
85
93
  "workspace.expand": "Open workspace",
86
94
  "workspace.collapse": "Collapse workspace",
87
95
  "workspace.refresh": "Refresh",
@@ -490,6 +498,35 @@ const I18n = (() => {
490
498
  "settings.models.empty": "No models configured. Click \"+ Add Model\" to add one.",
491
499
  "settings.models.badge.default": "Default",
492
500
  "settings.models.badge.lite": "Lite",
501
+ "settings.media.title": "Media Generation",
502
+ "settings.media.desc": "Optional. Image / video / audio generation models.",
503
+ "settings.media.loading": "Loading…",
504
+ "settings.media.error": "Failed to load: {{msg}}",
505
+ "settings.media.kind.image": "Image",
506
+ "settings.media.kind.video": "Video",
507
+ "settings.media.kind.audio": "Audio",
508
+ "settings.media.source.off": "Off",
509
+ "settings.media.source.auto": "Auto",
510
+ "settings.media.source.custom": "Custom",
511
+ "settings.media.field.model": "Model",
512
+ "settings.media.field.baseUrl": "Base URL",
513
+ "settings.media.field.apiKey": "API Key",
514
+ "settings.media.field.provider":"Provider",
515
+ "settings.media.off.hint": "Disabled.",
516
+ "settings.media.auto.followsDefault": "Follows default chat model",
517
+ "settings.media.auto.noDefaultModel": "Set a default chat model first.",
518
+ "settings.media.auto.unsupported": "Current provider has no built-in model for this kind. Switch to Custom.",
519
+ "settings.media.auto.comingSoon": "Not available yet — no built-in providers.",
520
+ "settings.media.auto.disabledTitle": "No built-in provider available — use Custom.",
521
+ "settings.media.action.edit": "Edit",
522
+ "settings.media.action.save": "Save",
523
+ "settings.media.action.cancel": "Cancel",
524
+ "settings.media.action.saving": "Saving…",
525
+ "settings.media.action.saved": "Saved",
526
+ "settings.media.apiKey.placeholder": "Enter API key",
527
+ "settings.media.apiKey.required": "API key required",
528
+ "settings.media.model.required": "Model name required",
529
+ "settings.media.baseUrl.required": "Base URL required",
493
530
  "settings.models.field.quicksetup": "Quick Setup",
494
531
  "settings.models.field.model": "Model",
495
532
  "settings.models.field.baseurl": "Base URL",
@@ -595,6 +632,10 @@ const I18n = (() => {
595
632
  "settings.currency.usd": "$ USD",
596
633
  "settings.currency.cny": "¥ CNY",
597
634
  "settings.currency.exchangeRate": "Exchange Rate (1 USD =)",
635
+ "settings.currency.updateLatest": "Update latest",
636
+ "settings.currency.updating": "Updating…",
637
+ "settings.currency.updated": "Updated from {{source}} on {{date}}",
638
+ "settings.currency.updateFailed": "Failed to fetch the latest rate. You can still enter it manually.",
598
639
 
599
640
  // ── Onboard ──
600
641
  "onboard.title": "Welcome to {{brand}}",
@@ -771,8 +812,16 @@ const I18n = (() => {
771
812
  "sessions.actions.download": "下载会话文件",
772
813
  "sessions.actions.downloadHint": "用于调试",
773
814
  "sib.dir.tooltip": "点击切换工作目录",
774
- "sib.dir.changePrompt": "切换工作目录:",
775
- "workspace.title": "工作区",
815
+ "sib.dir.changePrompt": "切换工作目录",
816
+ "sib.dir.home": "主目录",
817
+ "sib.dir.root": "根目录",
818
+ "sib.dir.default": "默认工作目录",
819
+ "sib.dir.current": "当前工作目录", "sib.dir.inputPlaceholder": "输入或选择目录路径",
820
+ "sib.dir.loading": "加载中…",
821
+ "sib.dir.empty": "空目录",
822
+ "sib.dir.loadError": "加载失败",
823
+ "sib.dir.confirm": "确认",
824
+ "sib.dir.cancel": "取消", "workspace.title": "工作区",
776
825
  "workspace.expand": "打开工作区",
777
826
  "workspace.collapse": "收起工作区",
778
827
  "workspace.refresh": "刷新",
@@ -1180,6 +1229,35 @@ const I18n = (() => {
1180
1229
  "settings.models.empty": "暂未配置模型,点击「+ 添加模型」添加。",
1181
1230
  "settings.models.badge.default": "默认",
1182
1231
  "settings.models.badge.lite": "轻量",
1232
+ "settings.media.title": "媒体生成",
1233
+ "settings.media.desc": "可选。图片 / 视频 / 音频 生成模型。",
1234
+ "settings.media.loading": "加载中…",
1235
+ "settings.media.error": "加载失败:{{msg}}",
1236
+ "settings.media.kind.image": "图片",
1237
+ "settings.media.kind.video": "视频",
1238
+ "settings.media.kind.audio": "音频",
1239
+ "settings.media.source.off": "关闭",
1240
+ "settings.media.source.auto": "自动",
1241
+ "settings.media.source.custom": "自定义",
1242
+ "settings.media.field.model": "模型",
1243
+ "settings.media.field.baseUrl": "Base URL",
1244
+ "settings.media.field.apiKey": "API Key",
1245
+ "settings.media.field.provider":"服务商",
1246
+ "settings.media.off.hint": "已关闭。",
1247
+ "settings.media.auto.followsDefault": "跟随默认聊天模型",
1248
+ "settings.media.auto.noDefaultModel": "请先设置默认聊天模型。",
1249
+ "settings.media.auto.unsupported": "当前服务商无内置模型,请切换到「自定义」。",
1250
+ "settings.media.auto.comingSoon": "暂无内置服务商,敬请期待。",
1251
+ "settings.media.auto.disabledTitle": "暂无内置服务商,请使用自定义。",
1252
+ "settings.media.action.edit": "编辑",
1253
+ "settings.media.action.save": "保存",
1254
+ "settings.media.action.cancel": "取消",
1255
+ "settings.media.action.saving": "保存中…",
1256
+ "settings.media.action.saved": "已保存",
1257
+ "settings.media.apiKey.placeholder": "请输入 API Key",
1258
+ "settings.media.apiKey.required": "请填写 API Key",
1259
+ "settings.media.model.required": "请填写模型名称",
1260
+ "settings.media.baseUrl.required": "请填写 Base URL",
1183
1261
  "settings.models.field.quicksetup": "快速配置",
1184
1262
  "settings.models.field.model": "Model",
1185
1263
  "settings.models.field.baseurl": "Base URL",
@@ -1285,6 +1363,10 @@ const I18n = (() => {
1285
1363
  "settings.currency.usd": "$ 美元",
1286
1364
  "settings.currency.cny": "¥ 人民币",
1287
1365
  "settings.currency.exchangeRate": "汇率 (1 美元 =)",
1366
+ "settings.currency.updateLatest": "获取最新汇率",
1367
+ "settings.currency.updating": "获取中…",
1368
+ "settings.currency.updated": "已从 {{source}} 更新,日期 {{date}}",
1369
+ "settings.currency.updateFailed": "获取最新汇率失败,仍可手动输入。",
1288
1370
 
1289
1371
  // ── Onboard ──
1290
1372
  "onboard.title": "欢迎使用 {{brand}}",
@@ -384,7 +384,7 @@
384
384
  <div id="image-preview-strip" style="display:none"></div>
385
385
  <div id="input-bar">
386
386
  <!-- Hidden file picker -->
387
- <input type="file" id="image-file-input" accept="image/png,image/jpeg,image/gif,image/webp,*/*" multiple style="display:none">
387
+ <input type="file" id="image-file-input" accept="*/*" multiple style="display:none">
388
388
  <button id="btn-attach" title="Attach file (image, pdf, docx, md, tar.gz…) — drag & drop / Ctrl+V also work">
389
389
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
390
390
  <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
@@ -771,6 +771,17 @@
771
771
  </div>
772
772
  <div id="model-cards"></div>
773
773
  </section>
774
+
775
+ <!-- Media generation section -->
776
+ <section class="settings-section" id="media-section">
777
+ <div class="settings-section-title">
778
+ <span data-i18n="settings.media.title">Media Generation</span>
779
+ </div>
780
+ <div class="settings-section-desc" data-i18n="settings.media.desc">
781
+ Generate images, video, and audio using your configured providers. "Auto" follows your default chat model.
782
+ </div>
783
+ <div id="media-rows"></div>
784
+ </section>
774
785
  </div>
775
786
 
776
787
  <!-- ══ Tab: UI ══ -->
@@ -813,7 +824,15 @@
813
824
  <div class="settings-exchange-rate-input-wrapper">
814
825
  <input type="number" id="settings-exchange-rate" class="settings-exchange-rate-input" step="0.0001" min="0.0001" placeholder="6.7944">
815
826
  <span class="settings-exchange-rate-unit">CNY</span>
827
+ <button type="button" id="btn-update-exchange-rate" class="settings-exchange-rate-refresh">
828
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
829
+ <path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
830
+ <path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
831
+ </svg>
832
+ <span data-i18n="settings.currency.updateLatest">Update latest</span>
833
+ </button>
816
834
  </div>
835
+ <div id="settings-exchange-rate-status" class="settings-exchange-rate-status"></div>
817
836
  </label>
818
837
  </div>
819
838
  </section>
@@ -1052,8 +1071,8 @@
1052
1071
  <div id="model-modal-test-result" class="model-test-result"></div>
1053
1072
 
1054
1073
  <label class="model-field model-field-checkbox" id="model-modal-default-field">
1055
- <input type="checkbox" id="model-modal-set-default">
1056
- <span data-i18n="settings.models.field.setDefault">Set as default model</span>
1074
+ <input type="checkbox" id="model-modal-set-default" class="field-checkbox">
1075
+ <span class="field-label" data-i18n="settings.models.field.setDefault">Set as default model</span>
1057
1076
  </label>
1058
1077
  </div>
1059
1078
  <div class="modal-footer">
@@ -1288,6 +1307,7 @@
1288
1307
  <script src="/skills.js"></script>
1289
1308
  <script src="/channels.js"></script>
1290
1309
  <script src="/mcp.js"></script>
1310
+ <script src="/model-tester.js"></script>
1291
1311
  <script src="/settings.js"></script>
1292
1312
  <script src="/billing.js"></script>
1293
1313
  <script src="/onboard.js"></script>