mulmoclaude 0.9.2 → 0.9.4
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.
- package/bin/mulmoclaude.js +2 -1
- package/client/assets/{index-Dc0R-HW5.js → index-B3NxFcEH.js} +46 -46
- package/client/assets/{index-Dxo1Zdd-.css → index-BXb--as6.css} +1 -1
- package/client/assets/{marp-CSq0PPfK.js → marp-C9QDHFAJ.js} +1 -1
- package/client/index.html +2 -2
- package/package.json +11 -10
- package/server/api/routes/agent.ts +5 -1
- package/server/api/routes/chat-index.ts +5 -1
- package/server/api/routes/collections.ts +152 -1
- package/server/api/routes/config.ts +12 -0
- package/server/index.ts +14 -2
- package/server/prompts/system/system.md +7 -1
- package/server/remoteHost/firebase.ts +6 -0
- package/server/remoteHost/handlers/collectionPage.ts +15 -17
- package/server/remoteHost/handlers/getCollection.ts +2 -2
- package/server/remoteHost/handlers/getFeed.ts +2 -2
- package/server/remoteHost/handlers/getRemoteView.ts +37 -0
- package/server/remoteHost/handlers/getRemoteViewItems.ts +38 -0
- package/server/remoteHost/handlers/index.ts +10 -0
- package/server/remoteHost/handlers/ingestAttachments.ts +88 -0
- package/server/remoteHost/handlers/listAccountingBooks.ts +31 -0
- package/server/remoteHost/handlers/listCollections.ts +6 -1
- package/server/remoteHost/handlers/listSkills.ts +39 -0
- package/server/remoteHost/handlers/mutateRemoteView.ts +40 -0
- package/server/remoteHost/handlers/startChat.ts +103 -39
- package/server/system/config.ts +101 -4
- package/server/system/logs/aaa +737 -0
- package/server/system/logs/bb +446 -0
- package/server/utils/files/thumbnail-store.ts +97 -0
- package/server/workspace/chat-index/index.ts +29 -3
- package/server/workspace/chat-index/indexer.ts +187 -29
- package/server/workspace/chat-index/summarizer.ts +18 -10
- package/server/workspace/collections/index.ts +2 -1
- package/server/workspace/collections/remoteView.ts +290 -0
- package/server/workspace/journal/archivist-cli.ts +17 -3
- package/server/workspace/journal/dailyPass.ts +52 -2
- package/server/workspace/journal/index.ts +20 -4
- package/src/App.vue +62 -3
- package/src/components/FileTree.vue +21 -1
- package/src/components/FileTreePane.vue +41 -25
- package/src/components/FilesView.vue +4 -0
- package/src/components/RemoteHostControl.vue +18 -0
- package/src/components/SessionHistoryPanel.vue +24 -11
- package/src/components/SettingsChatIndexTab.vue +119 -0
- package/src/components/SettingsJournalTab.vue +117 -0
- package/src/components/SettingsModal.vue +12 -2
- package/src/composables/collections/uiHost.ts +15 -0
- package/src/composables/collections/useDynamicShortcutIcons.ts +93 -0
- package/src/composables/usePubSub.ts +40 -2
- package/src/composables/useSessionSync.ts +9 -0
- package/src/composables/useShowHiddenSystemFiles.ts +23 -0
- package/src/config/apiRoutes.ts +20 -0
- package/src/config/roles.ts +2 -2
- package/src/config/visibleWorkspaceDirs.ts +21 -0
- package/src/lang/de.ts +47 -0
- package/src/lang/en.ts +45 -0
- package/src/lang/es.ts +46 -0
- package/src/lang/fr.ts +47 -0
- package/src/lang/ja.ts +46 -0
- package/src/lang/ko.ts +45 -0
- package/src/lang/pt-BR.ts +46 -0
- package/src/lang/zh.ts +44 -0
- package/src/plugins/textResponse/View.vue +49 -14
- package/src/plugins/textResponse/utils.ts +37 -0
- package/src/utils/html/previewCsp.ts +7 -16
- package/src/utils/role/icon.ts +9 -2
- package/src/utils/session/sessionPreview.ts +29 -0
package/src/lang/ko.ts
CHANGED
|
@@ -141,6 +141,9 @@ const koMessages = {
|
|
|
141
141
|
disconnectFailed: "연결 해제 실패",
|
|
142
142
|
signInFailed: "Google 로그인 실패",
|
|
143
143
|
statusFailed: "상태를 불러오지 못했습니다",
|
|
144
|
+
description: "원격 액세스를 사용하면 모바일 기기에서 이 MulmoClaude의 컬렉션과 피드에 연결할 수 있습니다.",
|
|
145
|
+
howTo: "휴대폰에서 {url} 을(를) 열고 같은 Google 계정으로 로그인하세요.",
|
|
146
|
+
customViewHint: "모바일에 최적화된 뷰가 필요하다면, 일반 custom view가 아닌 {keyword} 를(을) 만들어 달라고 Claude에게 요청하세요.",
|
|
144
147
|
},
|
|
145
148
|
sidebarHeader: {
|
|
146
149
|
home: "최신 채팅으로 이동",
|
|
@@ -184,6 +187,8 @@ const koMessages = {
|
|
|
184
187
|
reference: "참조",
|
|
185
188
|
// "RO" = Read-Only. 간결한 배지로 표시하기 위해 영문 약어 유지.
|
|
186
189
|
readOnlyBadge: "RO",
|
|
190
|
+
showSystemFiles: "시스템 파일 표시",
|
|
191
|
+
showSystemFilesTitle: "사용자 콘텐츠(data/, artifacts/, config/)에 더해 에이전트 내부 최상위 디렉터리(conversations/, feeds/ 등)까지 표시합니다.",
|
|
187
192
|
},
|
|
188
193
|
fileTree: {
|
|
189
194
|
workspace: "(워크스페이스)",
|
|
@@ -235,6 +240,8 @@ const koMessages = {
|
|
|
235
240
|
photos: "사진",
|
|
236
241
|
model: "모델",
|
|
237
242
|
voice: "음성",
|
|
243
|
+
chatIndex: "채팅 인덱스",
|
|
244
|
+
journal: "저널",
|
|
238
245
|
skills: "스킬",
|
|
239
246
|
roles: "역할",
|
|
240
247
|
},
|
|
@@ -292,6 +299,42 @@ const koMessages = {
|
|
|
292
299
|
loadError: "설정을 불러오지 못했습니다",
|
|
293
300
|
saveError: "저장에 실패했습니다",
|
|
294
301
|
},
|
|
302
|
+
chatIndexTab: {
|
|
303
|
+
description:
|
|
304
|
+
"채팅 기록의 AI 제목/요약 자동 생성을 설정합니다. 기본값은 off 이며, 자동화 계열 세션(scheduler / 시스템 워커)은 on 상태여도 항상 제외됩니다. 사람 세션만 턴 종료 시 요약이 한 번 실행됩니다.",
|
|
305
|
+
modeLabel: "채팅 인덱스 모델",
|
|
306
|
+
helperText: "Haiku 가 더 저렴하고, Sonnet 은 길고 주제가 바뀌는 세션에서 더 정확한 제목을 만듭니다.",
|
|
307
|
+
mode: {
|
|
308
|
+
off: "꺼짐",
|
|
309
|
+
haiku: "Haiku",
|
|
310
|
+
sonnet: "Sonnet",
|
|
311
|
+
},
|
|
312
|
+
status: {
|
|
313
|
+
off: "인덱싱: 꺼짐",
|
|
314
|
+
haiku: "Haiku 로 인덱싱 중",
|
|
315
|
+
sonnet: "Sonnet 으로 인덱싱 중",
|
|
316
|
+
},
|
|
317
|
+
loadError: "설정을 불러오지 못했습니다",
|
|
318
|
+
saveError: "저장에 실패했습니다",
|
|
319
|
+
},
|
|
320
|
+
journalTab: {
|
|
321
|
+
description:
|
|
322
|
+
"일일 저널 자동 생성 설정입니다. 최근 채팅 세션을 journal/*.md 로 요약하고 지속 메모(memory.md)를 추출합니다. 기본값은 off. 자동화 세션(scheduler / 시스템 워커)은 이 설정과 무관하게 항상 제외됩니다.",
|
|
323
|
+
modeLabel: "저널 모델",
|
|
324
|
+
helperText: "Haiku 가 더 저렴하고, Sonnet 은 일일/주제 요약 품질이 더 좋습니다. 매시 실행은 이 설정이 켜져 있을 때만 동작합니다.",
|
|
325
|
+
mode: {
|
|
326
|
+
off: "꺼짐",
|
|
327
|
+
haiku: "Haiku",
|
|
328
|
+
sonnet: "Sonnet",
|
|
329
|
+
},
|
|
330
|
+
status: {
|
|
331
|
+
off: "저널: 꺼짐",
|
|
332
|
+
haiku: "Haiku 로 저널 실행 중",
|
|
333
|
+
sonnet: "Sonnet 으로 저널 실행 중",
|
|
334
|
+
},
|
|
335
|
+
loadError: "설정을 불러오지 못했습니다",
|
|
336
|
+
saveError: "저장에 실패했습니다",
|
|
337
|
+
},
|
|
295
338
|
// `<i18n-t>` 슬롯 — `envKey` / `envFile` 은 SettingsModal.vue 에서
|
|
296
339
|
// 인라인 `<code>` 로 렌더링되므로 변수명·파일명은 번역하지 않고
|
|
297
340
|
// 남깁니다.
|
|
@@ -935,6 +978,8 @@ const koMessages = {
|
|
|
935
978
|
cancel: "취소",
|
|
936
979
|
seededByPlugin: "{pkg}에서",
|
|
937
980
|
seededByPluginTooltip: "이 메시지는 사용자가 보낸 것이 아니라 {pkg} 플러그인에서 작성한 것입니다.",
|
|
981
|
+
truncatedForRender:
|
|
982
|
+
"이 메시지는 매우 깁니다(총 {total}자). 탭이 멈추지 않도록 앞부분만 표시됩니다 — {omitted}자 숨김. 전체 원문은 복사 버튼으로 가져올 수 있습니다.",
|
|
938
983
|
},
|
|
939
984
|
pluginSkill: {
|
|
940
985
|
noDescription: "(설명 없음)",
|
package/src/lang/pt-BR.ts
CHANGED
|
@@ -134,6 +134,9 @@ const ptBRMessages = {
|
|
|
134
134
|
disconnectFailed: "Falha ao desconectar",
|
|
135
135
|
signInFailed: "Falha ao entrar com o Google",
|
|
136
136
|
statusFailed: "Falha ao carregar o status",
|
|
137
|
+
description: "O acesso remoto permite que um dispositivo móvel se conecte às coleções e feeds deste MulmoClaude.",
|
|
138
|
+
howTo: "No seu telefone, abra {url} e entre com a mesma conta do Google.",
|
|
139
|
+
customViewHint: "Para uma visualização adaptada ao celular, peça ao Claude para criar uma {keyword} (não uma custom view comum).",
|
|
137
140
|
},
|
|
138
141
|
sidebarHeader: {
|
|
139
142
|
home: "Ir para o chat mais recente",
|
|
@@ -178,6 +181,9 @@ const ptBRMessages = {
|
|
|
178
181
|
// "RO" = Read-Only. Abreviação em inglês mantida para caber como
|
|
179
182
|
// badge compacto ao lado do rótulo Referência.
|
|
180
183
|
readOnlyBadge: "RO",
|
|
184
|
+
showSystemFiles: "Exibir arquivos do sistema",
|
|
185
|
+
showSystemFilesTitle:
|
|
186
|
+
"Mostra os diretórios raiz internos do agente (conversations/, feeds/ etc.) além do conteúdo do usuário (data/, artifacts/, config/).",
|
|
181
187
|
},
|
|
182
188
|
fileTree: {
|
|
183
189
|
workspace: "(workspace)",
|
|
@@ -229,6 +235,8 @@ const ptBRMessages = {
|
|
|
229
235
|
photos: "Fotos",
|
|
230
236
|
model: "Modelo",
|
|
231
237
|
voice: "Voz",
|
|
238
|
+
chatIndex: "Índice de chat",
|
|
239
|
+
journal: "Diário",
|
|
232
240
|
skills: "Skills",
|
|
233
241
|
roles: "Papéis",
|
|
234
242
|
},
|
|
@@ -288,6 +296,42 @@ const ptBRMessages = {
|
|
|
288
296
|
loadError: "Falha ao carregar as configurações",
|
|
289
297
|
saveError: "Falha ao salvar",
|
|
290
298
|
},
|
|
299
|
+
chatIndexTab: {
|
|
300
|
+
description:
|
|
301
|
+
"Títulos e resumos gerados por IA para o histórico do chat. Sai desativado por padrão — sessões de automação (scheduler / workers do sistema) sempre são ignoradas mesmo quando ativado; sessões humanas pagam apenas uma chamada ao sumarizador ao terminar cada turno.",
|
|
302
|
+
modeLabel: "Modelo do índice de chat",
|
|
303
|
+
helperText: "Haiku é mais barato; Sonnet dá títulos mais precisos em sessões longas que mudam de assunto.",
|
|
304
|
+
mode: {
|
|
305
|
+
off: "Desativado",
|
|
306
|
+
haiku: "Haiku",
|
|
307
|
+
sonnet: "Sonnet",
|
|
308
|
+
},
|
|
309
|
+
status: {
|
|
310
|
+
off: "Indexação DESATIVADA",
|
|
311
|
+
haiku: "Indexando com Haiku",
|
|
312
|
+
sonnet: "Indexando com Sonnet",
|
|
313
|
+
},
|
|
314
|
+
loadError: "Falha ao carregar as configurações",
|
|
315
|
+
saveError: "Falha ao salvar",
|
|
316
|
+
},
|
|
317
|
+
journalTab: {
|
|
318
|
+
description:
|
|
319
|
+
"Diário diário automatizado — resume sessões recentes de chat em journal/*.md e extrai notas de memória duradoura. Sai desativado por padrão. Sessões de automação (scheduler / workers do sistema) são sempre excluídas, independentemente desta configuração.",
|
|
320
|
+
modeLabel: "Modelo do diário",
|
|
321
|
+
helperText: "Haiku é mais barato; Sonnet produz resumos diários / por tópico mais ricos. A passagem horária só é executada quando isto está ativado.",
|
|
322
|
+
mode: {
|
|
323
|
+
off: "Desativado",
|
|
324
|
+
haiku: "Haiku",
|
|
325
|
+
sonnet: "Sonnet",
|
|
326
|
+
},
|
|
327
|
+
status: {
|
|
328
|
+
off: "Diário DESATIVADO",
|
|
329
|
+
haiku: "Diário em execução com Haiku",
|
|
330
|
+
sonnet: "Diário em execução com Sonnet",
|
|
331
|
+
},
|
|
332
|
+
loadError: "Falha ao carregar as configurações",
|
|
333
|
+
saveError: "Falha ao salvar",
|
|
334
|
+
},
|
|
291
335
|
// Slots `<i18n-t>` — os nomes `envKey` / `envFile` renderizam como
|
|
292
336
|
// `<code>` inline no SettingsModal.vue, então os literais de
|
|
293
337
|
// variável e nome de arquivo permanecem não traduzidos.
|
|
@@ -936,6 +980,8 @@ const ptBRMessages = {
|
|
|
936
980
|
cancel: "Cancelar",
|
|
937
981
|
seededByPlugin: "de {pkg}",
|
|
938
982
|
seededByPluginTooltip: "Esta mensagem foi gerada pelo plugin {pkg}, não foi enviada por você.",
|
|
983
|
+
truncatedForRender:
|
|
984
|
+
"Esta mensagem é excepcionalmente longa ({total} caracteres no total). Apenas a primeira parte é renderizada — {omitted} caracteres ocultos para manter a aba responsiva. Use Copiar para obter o texto completo.",
|
|
939
985
|
},
|
|
940
986
|
pluginSkill: {
|
|
941
987
|
noDescription: "(sem descrição)",
|
package/src/lang/zh.ts
CHANGED
|
@@ -136,6 +136,9 @@ const zhMessages = {
|
|
|
136
136
|
disconnectFailed: "断开连接失败",
|
|
137
137
|
signInFailed: "Google 登录失败",
|
|
138
138
|
statusFailed: "加载状态失败",
|
|
139
|
+
description: "远程访问允许移动设备连接到此 MulmoClaude 的收藏与消息流。",
|
|
140
|
+
howTo: "在手机上打开 {url},用同一个 Google 账号登录。",
|
|
141
|
+
customViewHint: "如需移动端优化的视图,请让 Claude 创建 {keyword}(而不是普通的 custom view)。",
|
|
139
142
|
},
|
|
140
143
|
sidebarHeader: {
|
|
141
144
|
home: "前往最新对话",
|
|
@@ -179,6 +182,8 @@ const zhMessages = {
|
|
|
179
182
|
reference: "引用",
|
|
180
183
|
// "RO" = Read-Only. 保留英文缩写作为紧凑徽章。
|
|
181
184
|
readOnlyBadge: "RO",
|
|
185
|
+
showSystemFiles: "显示系统文件",
|
|
186
|
+
showSystemFilesTitle: "在用户内容(data/、artifacts/、config/)之外同时显示代理内部的顶层目录(conversations/、feeds/ 等)。",
|
|
182
187
|
},
|
|
183
188
|
fileTree: {
|
|
184
189
|
workspace: "(工作区)",
|
|
@@ -230,6 +235,8 @@ const zhMessages = {
|
|
|
230
235
|
photos: "照片",
|
|
231
236
|
model: "模型",
|
|
232
237
|
voice: "语音",
|
|
238
|
+
chatIndex: "聊天索引",
|
|
239
|
+
journal: "日志",
|
|
233
240
|
skills: "技能",
|
|
234
241
|
roles: "角色",
|
|
235
242
|
},
|
|
@@ -286,6 +293,42 @@ const zhMessages = {
|
|
|
286
293
|
loadError: "加载设置失败",
|
|
287
294
|
saveError: "保存失败",
|
|
288
295
|
},
|
|
296
|
+
chatIndexTab: {
|
|
297
|
+
description:
|
|
298
|
+
"为聊天历史自动生成 AI 标题/摘要。默认关闭。开启后自动化会话(scheduler / 系统 worker)仍会始终跳过;只有人类会话在每次轮次结束时才会调用一次摘要。",
|
|
299
|
+
modeLabel: "聊天索引模型",
|
|
300
|
+
helperText: "Haiku 更便宜;Sonnet 在长且话题多变的会话中标题更精准。",
|
|
301
|
+
mode: {
|
|
302
|
+
off: "关闭",
|
|
303
|
+
haiku: "Haiku",
|
|
304
|
+
sonnet: "Sonnet",
|
|
305
|
+
},
|
|
306
|
+
status: {
|
|
307
|
+
off: "索引已关闭",
|
|
308
|
+
haiku: "使用 Haiku 建立索引中",
|
|
309
|
+
sonnet: "使用 Sonnet 建立索引中",
|
|
310
|
+
},
|
|
311
|
+
loadError: "加载设置失败",
|
|
312
|
+
saveError: "保存失败",
|
|
313
|
+
},
|
|
314
|
+
journalTab: {
|
|
315
|
+
description:
|
|
316
|
+
"自动化每日日志 — 将近期聊天会话摘要为 journal/*.md,并抽取持久化记忆笔记。默认关闭。自动化会话(scheduler / 系统 worker)无论此设置如何都会始终排除。",
|
|
317
|
+
modeLabel: "日志模型",
|
|
318
|
+
helperText: "Haiku 更便宜;Sonnet 生成的每日/主题摘要更丰富。仅在此设置开启时每小时的轮次才会运行。",
|
|
319
|
+
mode: {
|
|
320
|
+
off: "关闭",
|
|
321
|
+
haiku: "Haiku",
|
|
322
|
+
sonnet: "Sonnet",
|
|
323
|
+
},
|
|
324
|
+
status: {
|
|
325
|
+
off: "日志已关闭",
|
|
326
|
+
haiku: "使用 Haiku 运行日志中",
|
|
327
|
+
sonnet: "使用 Sonnet 运行日志中",
|
|
328
|
+
},
|
|
329
|
+
loadError: "加载设置失败",
|
|
330
|
+
saveError: "保存失败",
|
|
331
|
+
},
|
|
289
332
|
// `<i18n-t>` 插槽 — 命名为 `envKey` / `envFile`,在 SettingsModal.vue
|
|
290
333
|
// 中作为行内 `<code>` 渲染,因此字面的变量名和文件名保持不翻译。
|
|
291
334
|
geminiRequired: "图像生成需要 {envKey}。请将它加入 {envFile} 并重启应用。",
|
|
@@ -925,6 +968,7 @@ const zhMessages = {
|
|
|
925
968
|
cancel: "取消",
|
|
926
969
|
seededByPlugin: "来自 {pkg}",
|
|
927
970
|
seededByPluginTooltip: "此消息由 {pkg} 插件生成,并非您发送。",
|
|
971
|
+
truncatedForRender: "该消息异常长(共 {total} 个字符)。为保持标签页响应,仅渲染开头部分 — 隐藏了 {omitted} 个字符。使用复制按钮获取完整原文。",
|
|
928
972
|
},
|
|
929
973
|
pluginSkill: {
|
|
930
974
|
noDescription: "(无描述)",
|
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
<span class="material-icons text-gray-400 text-base shrink-0 group-open:rotate-180 transition-transform">expand_more</span>
|
|
19
19
|
</summary>
|
|
20
20
|
<div class="border-t border-purple-200 p-4 bg-white rounded-b-lg">
|
|
21
|
+
<div
|
|
22
|
+
v-if="truncationInfo.wasTruncated"
|
|
23
|
+
class="mb-3 p-3 rounded border border-amber-300 bg-amber-50 text-amber-900 text-sm"
|
|
24
|
+
data-testid="text-response-seeded-truncation-banner"
|
|
25
|
+
>
|
|
26
|
+
{{
|
|
27
|
+
t("pluginTextResponse.truncatedForRender", {
|
|
28
|
+
omitted: truncationInfo.omittedChars.toLocaleString(locale),
|
|
29
|
+
total: truncationInfo.originalChars.toLocaleString(locale),
|
|
30
|
+
})
|
|
31
|
+
}}
|
|
32
|
+
</div>
|
|
21
33
|
<!-- eslint-disable vue/no-v-html -- marked.parse output of the plugin-seeded prompt; trusted in-process render matching the standard textResponse path. Multi-line element so disable/enable pair (CLAUDE.md UI rule). -->
|
|
22
34
|
<div ref="markdownContainerRef" class="markdown-content prose prose-slate max-w-none" @click="openLinksInNewTab" v-html="renderedHtml"></div>
|
|
23
35
|
<!-- eslint-enable vue/no-v-html -->
|
|
@@ -62,6 +74,18 @@
|
|
|
62
74
|
<span class="font-medium text-gray-700">{{ speakerLabel }}</span>
|
|
63
75
|
<span v-if="transportKind" class="italic">{{ transportKind }}</span>
|
|
64
76
|
</div>
|
|
77
|
+
<div
|
|
78
|
+
v-if="truncationInfo.wasTruncated"
|
|
79
|
+
class="mb-3 p-3 rounded border border-amber-300 bg-amber-50 text-amber-900 text-sm"
|
|
80
|
+
data-testid="text-response-truncation-banner"
|
|
81
|
+
>
|
|
82
|
+
{{
|
|
83
|
+
t("pluginTextResponse.truncatedForRender", {
|
|
84
|
+
omitted: truncationInfo.omittedChars.toLocaleString(locale),
|
|
85
|
+
total: truncationInfo.originalChars.toLocaleString(locale),
|
|
86
|
+
})
|
|
87
|
+
}}
|
|
88
|
+
</div>
|
|
65
89
|
<!-- eslint-disable vue/no-v-html -- marked.parse output of app-owned assistant response text; trusted in-process render. Multi-line element so disable/enable pair (CLAUDE.md UI rule) instead of -next-line. -->
|
|
66
90
|
<div
|
|
67
91
|
ref="markdownContainerRef"
|
|
@@ -110,9 +134,9 @@ import { usePdfDownload } from "../../composables/usePdfDownload";
|
|
|
110
134
|
import { useMarkdownZip } from "../../composables/useMarkdownZip";
|
|
111
135
|
import { useClipboardCopy } from "../../composables/useClipboardCopy";
|
|
112
136
|
import { buildPdfFilename } from "../../utils/files/filename";
|
|
113
|
-
import { extractTextResponseTitle } from "./utils";
|
|
137
|
+
import { extractTextResponseTitle, truncateForRender } from "./utils";
|
|
114
138
|
|
|
115
|
-
const { t } = useI18n();
|
|
139
|
+
const { t, locale } = useI18n();
|
|
116
140
|
const appApi = useAppApi();
|
|
117
141
|
|
|
118
142
|
const props = withDefaults(
|
|
@@ -162,19 +186,30 @@ const seededByPlugin = computed<string>(() => props.selectedResult.data?.seededB
|
|
|
162
186
|
// inherently scoped to the opening message.
|
|
163
187
|
const isSeededUserTurn = computed(() => Boolean(seededByPlugin.value) && messageRole.value === "user");
|
|
164
188
|
|
|
189
|
+
// Truncation summary surfaced to the template — the banner renders when
|
|
190
|
+
// `wasTruncated` is true, telling the user the visible content is a preview
|
|
191
|
+
// and the raw text is available via Copy. See #1863 for the pathological
|
|
192
|
+
// input this defends against (Opus 4.8 degenerate repetition freezing Safari).
|
|
193
|
+
const truncationInfo = computed(() => truncateForRender(messageText.value ?? ""));
|
|
194
|
+
|
|
165
195
|
const renderedHtml = computed(() => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
196
|
+
const { displayText } = truncationInfo.value;
|
|
197
|
+
if (!displayText) return "";
|
|
198
|
+
|
|
199
|
+
let processedText = displayText;
|
|
200
|
+
|
|
201
|
+
// Detect and wrap JSON content in code fences (skip for truncated views —
|
|
202
|
+
// JSON.parse of a truncated tail would throw anyway, and a partial JSON
|
|
203
|
+
// dump is more informative rendered as markdown).
|
|
204
|
+
if (!truncationInfo.value.wasTruncated) {
|
|
205
|
+
const trimmedText = processedText.trim();
|
|
206
|
+
if ((trimmedText.startsWith("{") && trimmedText.endsWith("}")) || (trimmedText.startsWith("[") && trimmedText.endsWith("]"))) {
|
|
207
|
+
try {
|
|
208
|
+
JSON.parse(trimmedText);
|
|
209
|
+
processedText = `\`\`\`json\n${trimmedText}\n\`\`\``;
|
|
210
|
+
} catch {
|
|
211
|
+
// Not valid JSON, continue with original text
|
|
212
|
+
}
|
|
178
213
|
}
|
|
179
214
|
}
|
|
180
215
|
|
|
@@ -4,6 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
const MAX_TITLE_CHARS = 50;
|
|
6
6
|
|
|
7
|
+
// Cap on how much of an assistant message we feed into marked() at once.
|
|
8
|
+
// The Opus 4.8 "degenerate repetition" bug can generate hundreds of
|
|
9
|
+
// thousands of chars of blank-line-separated single words; marked itself
|
|
10
|
+
// parses that fine (~120ms), but Safari's layout/paint on ~30k <p>
|
|
11
|
+
// elements freezes the tab for minutes (#1863). 100_000 is comfortably
|
|
12
|
+
// larger than any real assistant reply (Claude's ~200k token ceiling is
|
|
13
|
+
// well below that in bytes) and small enough that pathological input
|
|
14
|
+
// bounces off it well before the render blows up.
|
|
15
|
+
export const RENDER_TRUNCATE_CHARS = 100_000;
|
|
16
|
+
// The preview slice we DO render when a message trips the cap. Small
|
|
17
|
+
// enough that even a fully-blank-line-separated payload stays under a
|
|
18
|
+
// few thousand block elements. Users get "Copy" for the full raw text.
|
|
19
|
+
export const RENDER_TRUNCATE_PREVIEW_CHARS = 20_000;
|
|
20
|
+
|
|
21
|
+
export interface TruncationResult {
|
|
22
|
+
displayText: string;
|
|
23
|
+
wasTruncated: boolean;
|
|
24
|
+
originalChars: number;
|
|
25
|
+
omittedChars: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Truncate an assistant message so pathological model output can't
|
|
29
|
+
// freeze the render path. Pure so it can be unit-tested from
|
|
30
|
+
// node:test without a Vue runtime.
|
|
31
|
+
export function truncateForRender(text: string): TruncationResult {
|
|
32
|
+
const originalChars = text.length;
|
|
33
|
+
if (originalChars <= RENDER_TRUNCATE_CHARS) {
|
|
34
|
+
return { displayText: text, wasTruncated: false, originalChars, omittedChars: 0 };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
displayText: text.slice(0, RENDER_TRUNCATE_PREVIEW_CHARS),
|
|
38
|
+
wasTruncated: true,
|
|
39
|
+
originalChars,
|
|
40
|
+
omittedChars: originalChars - RENDER_TRUNCATE_PREVIEW_CHARS,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
// Pull a short, human-meaningful title out of a chat reply for use as
|
|
8
45
|
// a download filename. Priority:
|
|
9
46
|
// 1. First markdown H1 ("# ...") — the model often opens a long
|
|
@@ -5,23 +5,14 @@
|
|
|
5
5
|
// random `https://` origins, phone-home `fetch()` calls, etc. —
|
|
6
6
|
// is rejected.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// The list itself lives in `@mulmoclaude/core/remote-view`
|
|
9
|
+
// (SANDBOXED_VIEW_CDN_ALLOWLIST) so the remote-view CSP and these
|
|
10
|
+
// desktop policies can't drift — widen it THERE, and keep it
|
|
11
|
+
// audited: every entry is a potential supply-chain surface.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"https://cdnjs.cloudflare.com",
|
|
15
|
-
"https://fonts.googleapis.com",
|
|
16
|
-
"https://fonts.gstatic.com",
|
|
17
|
-
// Plotly's official CDN. The LLM defaults to this URL when it
|
|
18
|
-
// includes a Sankey or other Plotly chart in presentHtml output —
|
|
19
|
-
// Plotly's docs recommend it, so unconditioned LLM output ends up
|
|
20
|
-
// pointing here. Also reachable through jsdelivr, but adding the
|
|
21
|
-
// first-party CDN keeps historical artifacts (where the URL is
|
|
22
|
-
// already baked into the file on disk) rendering correctly.
|
|
23
|
-
"https://cdn.plot.ly",
|
|
24
|
-
];
|
|
13
|
+
import { SANDBOXED_VIEW_CDN_ALLOWLIST } from "@mulmoclaude/core/remote-view";
|
|
14
|
+
|
|
15
|
+
export const HTML_PREVIEW_CSP_ALLOWED_CDNS: readonly string[] = SANDBOXED_VIEW_CDN_ALLOWLIST;
|
|
25
16
|
|
|
26
17
|
/**
|
|
27
18
|
* Build the CSP string. Split from the wrapper so tests can exercise
|
package/src/utils/role/icon.ts
CHANGED
|
@@ -10,9 +10,16 @@ import type { Role } from "../../config/roles";
|
|
|
10
10
|
// don't render the literal text inside a Material Icons span.
|
|
11
11
|
const MATERIAL_ICON_RE = /^[a-z_]+$/;
|
|
12
12
|
|
|
13
|
+
// `smart_toy` (robot glyph) is used for both fallback cases —
|
|
14
|
+
// "role not found" and "role icon isn't a valid Material Icon name".
|
|
15
|
+
// Reserved specifically to avoid collision with `star`, which is the
|
|
16
|
+
// PinToggle glyph for collection shortcuts; using `star` here would
|
|
17
|
+
// make an unknown role look identical to a pinned collection (#1684).
|
|
18
|
+
const FALLBACK_ICON = "smart_toy";
|
|
19
|
+
|
|
13
20
|
export function roleIcon(roles: Role[], roleId: string): string {
|
|
14
|
-
const icon = roles.find((role) => role.id === roleId)?.icon ??
|
|
15
|
-
return MATERIAL_ICON_RE.test(icon) ? icon :
|
|
21
|
+
const icon = roles.find((role) => role.id === roleId)?.icon ?? FALLBACK_ICON;
|
|
22
|
+
return MATERIAL_ICON_RE.test(icon) ? icon : FALLBACK_ICON;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export function roleName(roles: Role[], roleId: string): string {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Selection helpers for the session-history sidebar row.
|
|
2
|
+
//
|
|
3
|
+
// The AI summary (chat indexer) is preferred over the raw first user
|
|
4
|
+
// message when it's meaningful. "Meaningful" excludes undefined / empty
|
|
5
|
+
// / whitespace-only strings — otherwise a summarizer that returns " "
|
|
6
|
+
// would blank out the row (no visible text, no tooltip, no aria label).
|
|
7
|
+
|
|
8
|
+
export interface SessionPreviewInput {
|
|
9
|
+
summary?: string;
|
|
10
|
+
preview: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveSessionSummary(summary: string | undefined): string | null {
|
|
14
|
+
const trimmed = summary?.trim();
|
|
15
|
+
return trimmed || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `null` means "nothing to show" — the template turns that into the
|
|
19
|
+
// localised `noMessages` placeholder. Not baked in here to keep this
|
|
20
|
+
// pure (no i18n dependency).
|
|
21
|
+
export function resolveSessionPrimaryText(session: SessionPreviewInput): string | null {
|
|
22
|
+
const summary = resolveSessionSummary(session.summary);
|
|
23
|
+
if (summary) return summary;
|
|
24
|
+
return session.preview || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionHasVisibleSummary(session: SessionPreviewInput): boolean {
|
|
28
|
+
return resolveSessionSummary(session.summary) !== null;
|
|
29
|
+
}
|