dsh-nextsteps 1.2.0 → 1.3.1
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/README.md +14 -11
- package/README.pt-BR.md +57 -0
- package/README.zh-CN.md +57 -0
- package/client.js +61 -10
- package/index.js +162 -7
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-nextsteps
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
|
|
5
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
3
|
+
English · [Português (Brasil)](README.pt-BR.md) · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-nextsteps) [](LICENSE) [](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
6
|
|
|
7
7
|
**Every finished agent turn ends with ready-to-click next steps.**
|
|
8
8
|
|
|
@@ -12,21 +12,24 @@
|
|
|
12
12
|
dsh plugin --profile web add dsh-nextsteps
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Product tour
|
|
16
|
+
|
|
17
|
+

|
|
16
18
|
|
|
17
|
-
1. The
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
19
|
+
1. Ask anything. The model works, answers, and calls the native `suggest_followups` tool right before its final answer.
|
|
20
|
+
2. The completed turn renders its own suggestion cards — attributed to that turn, never repeated across messages.
|
|
21
|
+
3. Click a card: the prompt is sent verbatim as your next queued message and the card flips to a sent state.
|
|
22
|
+
4. Everything survives restarts: cards, their turn attribution, and which ones you already clicked.
|
|
23
|
+
5. Open the dsh settings → **Nextsteps** to pick the generation language (15 languages or Auto), how many suggestions you get per turn (1–6), and whether the small header shows.
|
|
21
24
|
|
|
22
25
|
## Features
|
|
23
26
|
|
|
24
|
-
- Per-turn attribution — suggestions stay attached to the turn that produced them, across reloads.
|
|
27
|
+
- Per-turn attribution — suggestions stay attached to the turn that produced them, across reloads and app restarts.
|
|
25
28
|
- One-click send through the session's own conversation controller, with a composer fallback.
|
|
26
|
-
-
|
|
29
|
+
- Persistent captures: clicked cards stay marked after restarts (content-addressed keys, turn backfilled from history).
|
|
27
30
|
- Model-supplied labels, with automatic truncation when absent; strict payload validation with corrective model feedback.
|
|
28
31
|
- Settings page inside the dsh settings — language, followups per turn, header visibility, updates.
|
|
29
|
-
- Followups generated in the selected language
|
|
32
|
+
- Followups generated in the selected language — 15 languages or Auto, which follows the conversation.
|
|
30
33
|
- In-app updater — version check, one-click install, release notes, restart prompt.
|
|
31
34
|
- UI in English, Português (Brasil) and 简体中文.
|
|
32
35
|
|
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# dsh-nextsteps
|
|
2
|
+
|
|
3
|
+
[English](README.md) · Português (Brasil) · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-nextsteps) [](LICENSE) [](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
|
+
|
|
7
|
+
**Cada turno concluído do agente termina com próximos passos prontos para clicar.**
|
|
8
|
+
|
|
9
|
+
`dsh-nextsteps` é um plugin do [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) que faz o modelo sugerir cerca de três prompts de followup ao final de cada turno, renderiza-os como cards clicáveis sob o turno concluído e envia o prompt clicado como sua próxima mensagem — um clique, zero digitação.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
dsh plugin --profile web add dsh-nextsteps
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Tour do produto
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
1. Pergunte qualquer coisa. O modelo trabalha, responde e chama a tool nativa `suggest_followups` logo antes da resposta final.
|
|
20
|
+
2. O turno concluído renderiza seus próprios cards de sugestão — atribuídos àquele turno, nunca repetidos entre mensagens.
|
|
21
|
+
3. Clique num card: o prompt é enviado literalmente como sua próxima mensagem na fila e o card muda para o estado de enviado.
|
|
22
|
+
4. Tudo sobrevive a restarts: os cards, a atribuição de turno e os que você já clicou.
|
|
23
|
+
5. Abra as configurações do dsh → **Nextsteps** para escolher o idioma de geração (15 idiomas ou Automático), quantas sugestões por turno (1–6) e se o pequeno cabeçalho aparece.
|
|
24
|
+
|
|
25
|
+
## Funcionalidades
|
|
26
|
+
|
|
27
|
+
- Atribuição por turno — as sugestões ficam ligadas ao turno que as gerou, através de reloads e restarts do app.
|
|
28
|
+
- Envio com um clique pelo próprio controlador de conversa da sessão, com fallback pelo composer.
|
|
29
|
+
- Capturas persistentes: cards clicados continuam marcados após restarts (chaves endereçadas por conteúdo, turno retro-preenchido do histórico).
|
|
30
|
+
- Rótulos fornecidos pelo modelo, com truncamento automático quando ausentes; validação estrita do payload com feedback corretivo ao modelo.
|
|
31
|
+
- Página de configurações dentro das configurações do dsh — idioma, followups por turno, visibilidade do cabeçalho, atualizações.
|
|
32
|
+
- Followups gerados no idioma selecionado — 15 idiomas ou Automático, que segue a conversa.
|
|
33
|
+
- Atualizador in-app — verificação de versão, instalação com um clique, notas de release, pedido de reinício.
|
|
34
|
+
- Interface em English, Português (Brasil) e 简体中文.
|
|
35
|
+
|
|
36
|
+
## Configurações
|
|
37
|
+
|
|
38
|
+
Abra as configurações do dsh e encontre a seção **Nextsteps**:
|
|
39
|
+
|
|
40
|
+
| Configuração | O que faz |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Idioma | O idioma em que o modelo escreve os followups. `Automático` segue a conversa. |
|
|
43
|
+
| Followups por turno | Quantas sugestões o modelo produz (1–6). |
|
|
44
|
+
| Mostrar cabeçalho | O pequeno rótulo acima dos cards. |
|
|
45
|
+
|
|
46
|
+
## Atualizações
|
|
47
|
+
|
|
48
|
+
A mesma página de configurações verifica novos releases no GitHub. Quando uma versão mais nova é publicada no npm, **Atualizar agora** instala pelo gerenciador de plugins do dsh e pede reinício. As atualizações instalam somente o pacote npm `dsh-nextsteps` — cópias renomeadas ou de desenvolvimento são detectadas e deixadas em paz.
|
|
49
|
+
|
|
50
|
+
## Requisitos
|
|
51
|
+
|
|
52
|
+
- DeepSeek Harness 0.2.x (desktop, perfil web).
|
|
53
|
+
- Node.js 20+.
|
|
54
|
+
|
|
55
|
+
## Licença
|
|
56
|
+
|
|
57
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# dsh-nextsteps
|
|
2
|
+
|
|
3
|
+
[English](README.md) · [Português (Brasil)](README.pt-BR.md) · 简体中文
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-nextsteps) [](LICENSE) [](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
|
+
|
|
7
|
+
**每个智能体回合的结尾,都是可以一键继续的下一步。**
|
|
8
|
+
|
|
9
|
+
`dsh-nextsteps` 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的插件:模型在每个回合结束时建议大约三个后续提示,以可点击的卡片渲染在已完成的回合下方;点击后该提示即原样作为你的下一条消息发送——一次点击,无需输入。
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
dsh plugin --profile web add dsh-nextsteps
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 产品导览
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
1. 随意提问。模型完成工作、给出回答,并在最终回答之前调用原生的 `suggest_followups` 工具。
|
|
20
|
+
2. 已完成的回合渲染出属于自己的建议卡片——归属于该回合,不会跨消息重复。
|
|
21
|
+
3. 点击卡片:提示原样作为你的下一条排队消息发送,卡片翻转为已发送状态。
|
|
22
|
+
4. 一切在重启后依然保留:卡片、回合归属、以及你已经点击过的那些。
|
|
23
|
+
5. 打开 dsh 设置 → **Nextsteps**,选择生成语言(15 种语言或"自动")、每回合的建议数量(1–6)、以及是否显示小标题。
|
|
24
|
+
|
|
25
|
+
## 功能
|
|
26
|
+
|
|
27
|
+
- 按回合归属——建议始终附着在产生它的回合上,跨刷新与应用重启。
|
|
28
|
+
- 通过会话自身的对话控制器一键发送,并有 composer 兜底。
|
|
29
|
+
- 持久化捕获:已点击的卡片在重启后保持标记(内容寻址键,回合由历史回填)。
|
|
30
|
+
- 模型提供的标签,缺失时自动截断;严格的负载校验,并向模型反馈纠错。
|
|
31
|
+
- dsh 设置内置配置页——语言、每回合建议数、标题可见性、更新。
|
|
32
|
+
- 以所选语言生成后续提示——15 种语言或"自动"(跟随对话语言)。
|
|
33
|
+
- 应用内更新器——版本检查、一键安装、发布说明、重启提示。
|
|
34
|
+
- 界面支持 English、Português (Brasil)、简体中文。
|
|
35
|
+
|
|
36
|
+
## 设置
|
|
37
|
+
|
|
38
|
+
打开 dsh 设置,找到 **Nextsteps** 部分:
|
|
39
|
+
|
|
40
|
+
| 设置 | 作用 |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| 语言 | 模型撰写后续提示使用的语言。`自动` 跟随对话语言。 |
|
|
43
|
+
| 每回合提示数 | 模型生成的建议数量(1–6)。 |
|
|
44
|
+
| 显示标题 | 卡片上方的小标题。 |
|
|
45
|
+
|
|
46
|
+
## 更新
|
|
47
|
+
|
|
48
|
+
同一个设置页会检查 GitHub 上的新版本。当 npm 上发布了更新的版本时,**立即更新** 会通过 dsh 插件管理器安装并提示重启。更新只会安装 npm 包 `dsh-nextsteps`——重命名或开发副本会被识别并保持不动。
|
|
49
|
+
|
|
50
|
+
## 环境要求
|
|
51
|
+
|
|
52
|
+
- DeepSeek Harness 0.2.x(桌面版,web profile)。
|
|
53
|
+
- Node.js 20+。
|
|
54
|
+
|
|
55
|
+
## 许可证
|
|
56
|
+
|
|
57
|
+
[MIT](LICENSE)
|
package/client.js
CHANGED
|
@@ -343,21 +343,44 @@ window.__ModuleLoader__.load({
|
|
|
343
343
|
{ value: "vi", label: "Tiếng Việt" }
|
|
344
344
|
];
|
|
345
345
|
const NPM_NAME = 'dsh-nextsteps';
|
|
346
|
+
const MODELS_ROUTE = '/api/dsh-nextsteps.models';
|
|
347
|
+
const GENERATE_ROUTE = '/api/dsh-nextsteps.generate';
|
|
348
|
+
let modelCatalogCache = undefined;
|
|
349
|
+
|
|
350
|
+
async function loadModelCatalog(force) {
|
|
351
|
+
if (modelCatalogCache === undefined || force === true) {
|
|
352
|
+
try {
|
|
353
|
+
const response = await fetch(MODELS_ROUTE, { cache: 'no-store' });
|
|
354
|
+
const data = await responseJson(response);
|
|
355
|
+
if (data !== null && typeof data === 'object' && data.ok === true) modelCatalogCache = data;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.warn('dsh-nextsteps: model catalog unavailable:', error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return modelCatalogCache;
|
|
361
|
+
}
|
|
346
362
|
const ICON_DATAURI = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%20role%3D%22img%22%20aria-label%3D%22Nextsteps%20icon%22%3E%0A%20%20%3Crect%20width%3D%22256%22%20height%3D%22256%22%20rx%3D%2256%22%20fill%3D%22%230d0d10%22%2F%3E%0A%20%20%3Crect%20x%3D%2252%22%20y%3D%2264%22%20width%3D%22152%22%20height%3D%2244%22%20rx%3D%2210%22%20fill%3D%22%231f242b%22%20stroke%3D%22%234b5563%22%20stroke-width%3D%223%22%2F%3E%0A%20%20%3Crect%20x%3D%2252%22%20y%3D%22124%22%20width%3D%22152%22%20height%3D%2244%22%20rx%3D%2210%22%20fill%3D%22%231f242b%22%20stroke%3D%22%234b5563%22%20stroke-width%3D%223%22%2F%3E%0A%20%20%3Crect%20x%3D%2252%22%20y%3D%22184%22%20width%3D%22152%22%20height%3D%2244%22%20rx%3D%2210%22%20fill%3D%22%23e5484d%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M84%20206h72M136%20194l14%2012-14%2012%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%228%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20fill%3D%22none%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M76%2086h96M76%20146h96%22%20stroke%3D%22%236b7280%22%20stroke-width%3D%227%22%20stroke-linecap%3D%22round%22%2F%3E%0A%3C%2Fsvg%3E%0A';
|
|
347
363
|
const SETTINGS_ROUTE = '/api/dsh-nextsteps.settings';
|
|
348
364
|
const UPDATE_ROUTE = '/api/dsh-nextsteps.update';
|
|
349
365
|
const DICTIONARIES = {
|
|
350
|
-
en: {"header":"Suggested followups","showEarlier":"Show earlier suggestions","hideEarlier":"Hide earlier suggestions","srFollowupSent":"Followup sent","sendFailed":"Could not send: ","intro":"Suggested next-step prompts under every finished agent turn — one click sends the followup.","setLanguage":"Language","setLanguageTip":"Followups are generated in this language. Auto follows the conversation.","optAuto":"Auto (follow the conversation)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setMax":"Followups per turn","setMaxTip":"How many suggested prompts the model should produce (1-6).","setHeader":"Show header","setHeaderTip":"The small “Suggested followups” label above the cards.","updates":"Updates","current":"Version","checking":"Checking…","upToDate":"up to date","available":"{v} is available","manualCopy":"Development or renamed copy — update it manually.","installing":"Installing {v}…","restartMsg":"Version {v} was installed. Restart the dsh to use it.","noReleases":"No releases published yet","checkFailed":"Update check failed ({e})","updateNow":"Update now","check":"Check for updates","openRelease":"Open release page","releaseNotes":"Release notes","repo":"GitHub repository","issues":"Report an issue"},
|
|
351
|
-
'pt-BR': {"header":"Sugestões de acompanhamento","showEarlier":"Mostrar sugestões anteriores","hideEarlier":"Ocultar sugestões anteriores","srFollowupSent":"Follow-up enviado","sendFailed":"Não consegui enviar: ","intro":"Sugestões de próximos passos sob cada turno concluído do agente — um clique envia o followup.","setLanguage":"Idioma","setLanguageTip":"Os followups são gerados neste idioma. Automático segue a conversa.","optAuto":"Automático (segue a conversa)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setMax":"Followups por turno","setMaxTip":"Quantas sugestões o modelo deve produzir (1-6).","setHeader":"Mostrar cabeçalho","setHeaderTip":"O rótulo pequeno “Sugestões de acompanhamento” acima dos cartões.","updates":"Atualizações","current":"Versão","checking":"Verificando…","upToDate":"em dia","available":"{v} disponível","manualCopy":"Cópia de desenvolvimento ou renomeada — atualize manualmente.","installing":"Instalando {v}…","restartMsg":"Versão {v} instalada. Reinicie o dsh para usá-la.","noReleases":"Nenhum release publicado ainda","checkFailed":"Falha ao verificar atualizações ({e})","updateNow":"Atualizar agora","check":"Verificar atualizações","openRelease":"Abrir página do release","releaseNotes":"Notas do release","repo":"Repositório no GitHub","issues":"Reportar um problema"},
|
|
352
|
-
'zh-CN': {"header":"建议的后续操作","showEarlier":"显示较早的建议","hideEarlier":"隐藏较早的建议","srFollowupSent":"后续操作已发送","sendFailed":"发送失败:","intro":"在每个智能体回合完成后的下方推荐下一步提示——单击即可发送。","setLanguage":"语言","setLanguageTip":"后续操作提示将以此语言生成。“自动”会跟随对话语言。","optAuto":"自动(跟随对话)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setMax":"每回合提示数","setMaxTip":"模型应生成的建议提示数量(1-6)。","setHeader":"显示标题","setHeaderTip":"卡片上方的小标题“建议的后续操作”。","updates":"更新","current":"版本","checking":"检查中…","upToDate":"已是最新","available":"{v} 可用","manualCopy":"开发或重命名的副本——请手动更新。","installing":"正在安装 {v}…","restartMsg":"版本 {v} 已安装。请重启 dsh 以使用。","noReleases":"尚未发布任何版本","checkFailed":"检查更新失败({e})","updateNow":"立即更新","check":"检查更新","openRelease":"打开发布页","releaseNotes":"发布说明","repo":"GitHub 仓库","issues":"反馈问题"}
|
|
366
|
+
en: {"header":"Suggested followups","showEarlier":"Show earlier suggestions","hideEarlier":"Hide earlier suggestions","srFollowupSent":"Followup sent","sendFailed":"Could not send: ","intro":"Suggested next-step prompts under every finished agent turn — one click sends the followup.","setLanguage":"Language","setLanguageTip":"Followups are generated in this language. Auto follows the conversation.","optAuto":"Auto (follow the conversation)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setModel":"Model","setModelTip":"The model that generates the suggestions. Auto keeps them inside the main turn.","optModelAuto":"Auto (same model as the chat)","setMax":"Followups per turn","setMaxTip":"How many suggested prompts the model should produce (1-6).","setHeader":"Show header","setHeaderTip":"The small “Suggested followups” label above the cards.","updates":"Updates","current":"Version","checking":"Checking…","upToDate":"up to date","available":"{v} is available","manualCopy":"Development or renamed copy — update it manually.","installing":"Installing {v}…","restartMsg":"Version {v} was installed. Restart the dsh to use it.","noReleases":"No releases published yet","checkFailed":"Update check failed ({e})","updateNow":"Update now","check":"Check for updates","openRelease":"Open release page","releaseNotes":"Release notes","repo":"GitHub repository","issues":"Report an issue"},
|
|
367
|
+
'pt-BR': {"header":"Sugestões de acompanhamento","showEarlier":"Mostrar sugestões anteriores","hideEarlier":"Ocultar sugestões anteriores","srFollowupSent":"Follow-up enviado","sendFailed":"Não consegui enviar: ","intro":"Sugestões de próximos passos sob cada turno concluído do agente — um clique envia o followup.","setLanguage":"Idioma","setLanguageTip":"Os followups são gerados neste idioma. Automático segue a conversa.","optAuto":"Automático (segue a conversa)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setModel":"Modelo","setModelTip":"O modelo que gera as sugestões. Automático as mantém no turno principal.","optModelAuto":"Automático (mesmo modelo da conversa)","setMax":"Followups por turno","setMaxTip":"Quantas sugestões o modelo deve produzir (1-6).","setHeader":"Mostrar cabeçalho","setHeaderTip":"O rótulo pequeno “Sugestões de acompanhamento” acima dos cartões.","updates":"Atualizações","current":"Versão","checking":"Verificando…","upToDate":"em dia","available":"{v} disponível","manualCopy":"Cópia de desenvolvimento ou renomeada — atualize manualmente.","installing":"Instalando {v}…","restartMsg":"Versão {v} instalada. Reinicie o dsh para usá-la.","noReleases":"Nenhum release publicado ainda","checkFailed":"Falha ao verificar atualizações ({e})","updateNow":"Atualizar agora","check":"Verificar atualizações","openRelease":"Abrir página do release","releaseNotes":"Notas do release","repo":"Repositório no GitHub","issues":"Reportar um problema"},
|
|
368
|
+
'zh-CN': {"header":"建议的后续操作","showEarlier":"显示较早的建议","hideEarlier":"隐藏较早的建议","srFollowupSent":"后续操作已发送","sendFailed":"发送失败:","intro":"在每个智能体回合完成后的下方推荐下一步提示——单击即可发送。","setLanguage":"语言","setLanguageTip":"后续操作提示将以此语言生成。“自动”会跟随对话语言。","optAuto":"自动(跟随对话)","optEn":"English","optPt":"Português (Brasil)","optZh":"简体中文","setModel":"模型","setModelTip":"生成建议的模型。自动会跟随对话所用的模型。","optModelAuto":"自动(与对话相同的模型)","setMax":"每回合提示数","setMaxTip":"模型应生成的建议提示数量(1-6)。","setHeader":"显示标题","setHeaderTip":"卡片上方的小标题“建议的后续操作”。","updates":"更新","current":"版本","checking":"检查中…","upToDate":"已是最新","available":"{v} 可用","manualCopy":"开发或重命名的副本——请手动更新。","installing":"正在安装 {v}…","restartMsg":"版本 {v} 已安装。请重启 dsh 以使用。","noReleases":"尚未发布任何版本","checkFailed":"检查更新失败({e})","updateNow":"立即更新","check":"检查更新","openRelease":"打开发布页","releaseNotes":"发布说明","repo":"GitHub 仓库","issues":"反馈问题"}
|
|
353
369
|
};
|
|
354
370
|
let pluginSettings = { lang: 'auto', maxFollowups: 3, showHeader: true };
|
|
355
371
|
let pluginVersion;
|
|
356
372
|
|
|
373
|
+
async function responseJson(response) {
|
|
374
|
+
try {
|
|
375
|
+
return await response.json();
|
|
376
|
+
} catch {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
357
380
|
async function refreshPluginSettings() {
|
|
358
381
|
try {
|
|
359
382
|
const response = await fetch(SETTINGS_ROUTE, { cache: 'no-store' });
|
|
360
|
-
const data = await response
|
|
383
|
+
const data = await responseJson(response);
|
|
361
384
|
if (data !== null && typeof data === 'object' && data.ok === true && data.settings !== null && typeof data.settings === 'object') {
|
|
362
385
|
pluginSettings = data.settings;
|
|
363
386
|
if (typeof data.version === 'string') pluginVersion = data.version;
|
|
@@ -456,6 +479,7 @@ window.__ModuleLoader__.load({
|
|
|
456
479
|
return function SuggestFollowupsTail(props) {
|
|
457
480
|
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : '';
|
|
458
481
|
const turn = turnIdOf(props.turn);
|
|
482
|
+
const afterSeq = props.turn !== null && typeof props.turn === 'object' && Number.isSafeInteger(props.turn.end?.seq) ? props.turn.end.seq : undefined;
|
|
459
483
|
const [, refresh] = React.useReducer((count) => count + 1, 0);
|
|
460
484
|
React.useEffect(() => {
|
|
461
485
|
captureListeners.add(refresh);
|
|
@@ -463,9 +487,26 @@ window.__ModuleLoader__.load({
|
|
|
463
487
|
captureListeners.delete(refresh);
|
|
464
488
|
};
|
|
465
489
|
}, []);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
490
|
+
const generatedRef = React.useRef(null);
|
|
491
|
+
React.useEffect(() => {
|
|
492
|
+
if (pluginSettings.model === '' || sessionId === '' || turn === undefined || afterSeq === undefined) return undefined;
|
|
493
|
+
const key = sessionId + '|' + turn;
|
|
494
|
+
if (generatedRef.current === key) return undefined;
|
|
495
|
+
if (captureForTurn(sessionId, turn) !== null) return undefined;
|
|
496
|
+
generatedRef.current = key;
|
|
497
|
+
let alive = true;
|
|
498
|
+
(async () => {
|
|
499
|
+
try {
|
|
500
|
+
const response = await fetch(GENERATE_ROUTE, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, afterSeq }) });
|
|
501
|
+
const data = await responseJson(response);
|
|
502
|
+
if (!alive || !response.ok || data === null || typeof data !== 'object' || !Array.isArray(data.followups) || data.followups.length === 0) return;
|
|
503
|
+
rememberCapture(sessionId, 'generated-' + turn, turn, validateFollowups(data.followups));
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.warn('dsh-nextsteps: generation failed:', error);
|
|
506
|
+
}
|
|
507
|
+
})();
|
|
508
|
+
return () => { alive = false; };
|
|
509
|
+
}, [sessionId, turn, afterSeq]);
|
|
469
510
|
let entry = turn !== undefined ? captureForTurn(sessionId, turn) : newestCapture(sessionId);
|
|
470
511
|
if (entry === null) {
|
|
471
512
|
const unclaimed = unclaimedCaptures(sessionId, turn);
|
|
@@ -490,13 +531,19 @@ window.__ModuleLoader__.load({
|
|
|
490
531
|
refreshPluginSettings().then((loaded) => setSettings({ ...loaded }));
|
|
491
532
|
check();
|
|
492
533
|
}, []);
|
|
534
|
+
const [models, setModels] = React.useState(() => modelCatalogCache);
|
|
535
|
+
React.useEffect(() => {
|
|
536
|
+
let alive = true;
|
|
537
|
+
loadModelCatalog(true).then((catalog) => { if (alive) setModels(catalog); });
|
|
538
|
+
return () => { alive = false; };
|
|
539
|
+
}, []);
|
|
493
540
|
const save = async (patch) => {
|
|
494
541
|
const merged = { ...settings, ...patch };
|
|
495
542
|
setSettings(merged);
|
|
496
543
|
try {
|
|
497
544
|
const response = await fetch(SETTINGS_ROUTE, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch) });
|
|
498
|
-
const data = await response
|
|
499
|
-
if (data !== null && typeof data === 'object' && data.ok === true && data.settings !== null && typeof data.settings === 'object') setSettings(data.settings);
|
|
545
|
+
const data = await responseJson(response);
|
|
546
|
+
if (data !== null && typeof data === 'object' && data.ok === true && data.settings !== null && typeof data.settings === 'object') { pluginSettings = data.settings; setSettings(data.settings); }
|
|
500
547
|
} catch (error) {
|
|
501
548
|
console.warn('dsh-nextsteps: settings save failed:', error);
|
|
502
549
|
}
|
|
@@ -505,7 +552,7 @@ window.__ModuleLoader__.load({
|
|
|
505
552
|
setUpdate((previous) => ({ ...previous, status: 'checking' }));
|
|
506
553
|
try {
|
|
507
554
|
const response = await fetch(UPDATE_ROUTE, { cache: 'no-store' });
|
|
508
|
-
const info = await response
|
|
555
|
+
const info = await responseJson(response);
|
|
509
556
|
if (!response.ok || info === null || typeof info !== 'object' || info.error !== undefined) throw new Error(info?.error ?? 'HTTP ' + response.status);
|
|
510
557
|
setUpdate({ status: info.newer === true ? (info.managed ? 'available' : 'manual') : 'current', info });
|
|
511
558
|
} catch (error) {
|
|
@@ -564,6 +611,10 @@ window.__ModuleLoader__.load({
|
|
|
564
611
|
row(t('setLanguage'), t('setLanguageTip'), h('select', { value: settings.lang, onChange: (event) => save({ lang: event.target.value }), style: controlStyle, 'aria-label': t('setLanguage') },
|
|
565
612
|
h('option', { value: 'auto' }, t('optAuto')),
|
|
566
613
|
...LANGUAGE_OPTIONS.map((option) => h('option', { key: option.value, value: option.value }, option.label))), 'lang'),
|
|
614
|
+
row(t('setModel'), t('setModelTip'), (models === undefined || (models.groups ?? []).length === 0) ? h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } }, t('checking')) : h('select', { value: settings.model, onChange: (event) => save({ model: event.target.value }), style: controlStyle, 'aria-label': t('setModel') },
|
|
615
|
+
h('option', { value: '' }, t('optModelAuto')),
|
|
616
|
+
...(models.groups ?? []).map((group) => h('optgroup', { key: group.id, label: group.name },
|
|
617
|
+
group.models.map((entry) => h('option', { key: group.id + '/' + entry.id, value: group.id + '/' + entry.id }, entry.name === entry.id ? entry.name : entry.name + ' (' + entry.id + ')'))))), 'model'),
|
|
567
618
|
row(t('setMax'), t('setMaxTip'), h('input', { type: 'number', min: 1, max: 6, value: settings.maxFollowups, onChange: (event) => save({ maxFollowups: Number(event.target.value) }), style: { ...controlStyle, width: 64 }, 'aria-label': t('setMax') }), 'max'),
|
|
568
619
|
row(t('setHeader'), t('setHeaderTip'), h('input', { type: 'checkbox', checked: settings.showHeader, onChange: (event) => save({ showHeader: event.target.checked }), 'aria-label': t('setHeader') }), 'header'),
|
|
569
620
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '14px 0', borderBottom: '.5px solid var(--dsw-alias-border-l1)', flexWrap: 'wrap' } },
|
package/index.js
CHANGED
|
@@ -6,11 +6,13 @@ import path from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
const TOOL_NAME = 'suggest_followups';
|
|
8
8
|
const name = 'dsh-nextsteps';
|
|
9
|
-
const inject = ['tools', 'connection'];
|
|
9
|
+
const inject = ['tools', 'connection', 'sessionQuery'];
|
|
10
10
|
const NPM_NAME = 'dsh-nextsteps';
|
|
11
11
|
const RELEASES_URL = 'https://api.github.com/repos/klaus-2/dsh-nextsteps/releases/latest';
|
|
12
12
|
const SETTINGS_FILE = path.join(homedir(), '.dsh', 'dsh-nextsteps', 'settings.json');
|
|
13
13
|
const PACKAGE = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
14
|
+
const GENERATE_TIMEOUT_MS = 30000;
|
|
15
|
+
const GENERATE_MAX_TOKENS = 500;
|
|
14
16
|
|
|
15
17
|
const LANGUAGE_NAMES = {
|
|
16
18
|
auto: "the same language as the conversation",
|
|
@@ -31,16 +33,25 @@ const LANGUAGE_NAMES = {
|
|
|
31
33
|
'vi': "Vietnamese"
|
|
32
34
|
};
|
|
33
35
|
|
|
36
|
+
function modelKeyOf(stored) {
|
|
37
|
+
if (stored === null || typeof stored !== 'object') return '';
|
|
38
|
+
if (typeof stored.model === 'string') return stored.model.includes('/') ? stored.model : '';
|
|
39
|
+
const provider = typeof stored.provider === 'string' ? stored.provider : '';
|
|
40
|
+
const model = typeof stored.model === 'string' ? stored.model : '';
|
|
41
|
+
return provider !== '' && model !== '' ? `${provider}/${model}` : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
function settingsLoad() {
|
|
35
45
|
try {
|
|
36
46
|
const stored = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')) ?? {};
|
|
37
47
|
return {
|
|
38
48
|
lang: ["auto","en","pt-BR","zh-CN","es","fr","de","it","ja","ko","ru","ar","hi","id","tr","vi"].includes(stored.lang) ? stored.lang : 'auto',
|
|
39
49
|
maxFollowups: Number.isInteger(stored.maxFollowups) && stored.maxFollowups >= 1 && stored.maxFollowups <= 6 ? stored.maxFollowups : 3,
|
|
40
|
-
showHeader: stored.showHeader !== false
|
|
50
|
+
showHeader: stored.showHeader !== false,
|
|
51
|
+
model: typeof stored.model === 'string' ? (stored.model.includes('/') ? stored.model : '') : modelKeyOf(stored)
|
|
41
52
|
};
|
|
42
53
|
} catch {
|
|
43
|
-
return { lang: 'auto', maxFollowups: 3, showHeader: true };
|
|
54
|
+
return { lang: 'auto', maxFollowups: 3, showHeader: true, model: '' };
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
|
@@ -156,6 +167,114 @@ function parseVersion(value) {
|
|
|
156
167
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
const get = (ctx, serviceName) => { try { return ctx.get?.(serviceName); } catch { return undefined; } };
|
|
171
|
+
|
|
172
|
+
async function modelCatalog(ctx) {
|
|
173
|
+
const llm = get(ctx, 'llm');
|
|
174
|
+
const fallback = get(ctx, 'agentDefaultModel')?.currentSelection?.();
|
|
175
|
+
const out = {
|
|
176
|
+
default: typeof fallback?.provider === 'string' && typeof fallback?.model === 'string' && fallback.provider !== '' && fallback.model !== ''
|
|
177
|
+
? { provider: fallback.provider, model: fallback.model }
|
|
178
|
+
: undefined,
|
|
179
|
+
groups: []
|
|
180
|
+
};
|
|
181
|
+
if (llm?.listProviders === undefined) return out;
|
|
182
|
+
for (const provider of llm.listProviders()) {
|
|
183
|
+
try {
|
|
184
|
+
const models = (await llm.listModels(provider.id)).map((entry) => ({ id: entry.id, name: entry.name }));
|
|
185
|
+
if (models.length > 0) out.groups.push({ id: provider.id, name: provider.name ?? provider.id, models });
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function turnTranscript(events, afterSeq) {
|
|
194
|
+
const lines = [];
|
|
195
|
+
for (const entry of events) {
|
|
196
|
+
const event = entry?.event ?? entry;
|
|
197
|
+
if (event === null || typeof event !== 'object') continue;
|
|
198
|
+
if (Number.isSafeInteger(afterSeq) && Number.isSafeInteger(event.seq) && event.seq > afterSeq) break;
|
|
199
|
+
if (event.type === 'user/message' && typeof event.data?.text === 'string') lines.push(`User: ${event.data.text.slice(0, 2000)}`);
|
|
200
|
+
else if (event.type === 'agent/message' && typeof event.data?.text === 'string') lines.push(`Assistant: ${event.data.text.slice(0, 2000)}`);
|
|
201
|
+
else if (event.type === 'tool/call' && typeof event.data?.name === 'string') lines.push(`Tool call: ${event.data.name}`);
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function generateSystem(settings) {
|
|
207
|
+
const language = LANGUAGE_NAMES[settings.lang] ?? LANGUAGE_NAMES.auto;
|
|
208
|
+
return [
|
|
209
|
+
'You suggest followup prompts the user of a coding-agent chat may want next. You answer with JSON only — no prose, no code fence.',
|
|
210
|
+
`Output shape: {"followups":[{"prompt":"...","label":"..."}]} with exactly ${settings.maxFollowups} items.`,
|
|
211
|
+
'Each prompt is one short sentence in the user\'s first-person voice, goal-oriented, standing alone without the transcript; each label is 2-5 words.',
|
|
212
|
+
`Write every prompt and label in ${language}.`,
|
|
213
|
+
'Good suggestions: alternatives to the latest change, related features, cleanup, tests, verification steps. If the transcript shows remaining plan steps, one suggestion may continue the next step.'
|
|
214
|
+
].join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function followupsFromModelText(text) {
|
|
218
|
+
if (typeof text !== 'string' || text === '') return undefined;
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(text.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim());
|
|
222
|
+
} catch {
|
|
223
|
+
const start = text.indexOf('{');
|
|
224
|
+
const end = text.lastIndexOf('}');
|
|
225
|
+
if (start === -1 || end <= start) return undefined;
|
|
226
|
+
try { parsed = JSON.parse(text.slice(start, end + 1)); } catch { return undefined; }
|
|
227
|
+
}
|
|
228
|
+
const followups = coerceFollowups(parsed);
|
|
229
|
+
if (followups === undefined) return undefined;
|
|
230
|
+
return followups.slice(0, 6).map((entry) => ({ prompt: String(entry.prompt).slice(0, 200), label: entry.label === null ? null : String(entry.label).slice(0, 40) }));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function generateForTurn(ctx, settings, sessionId, afterSeq, signal) {
|
|
234
|
+
const sessionQuery = get(ctx, 'sessionQuery');
|
|
235
|
+
if (sessionQuery?.observeSession === undefined) throw new Error('the sessionQuery service is unavailable');
|
|
236
|
+
const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'none' });
|
|
237
|
+
let transcript;
|
|
238
|
+
try {
|
|
239
|
+
const events = observation.events ?? [];
|
|
240
|
+
const last = events.at(-1)?.event ?? events.at(-1);
|
|
241
|
+
if (last?.type !== 'turn/end' || last.data?.reason?.kind !== 'completed' || last.seq !== afterSeq) {
|
|
242
|
+
throw Object.assign(new Error('the chat moved on'), { status: 409 });
|
|
243
|
+
}
|
|
244
|
+
transcript = turnTranscript(events, afterSeq);
|
|
245
|
+
} finally {
|
|
246
|
+
observation[Symbol.dispose]?.();
|
|
247
|
+
}
|
|
248
|
+
if (transcript.trim() === '') return { followups: [] };
|
|
249
|
+
const catalog = await modelCatalog(ctx);
|
|
250
|
+
const route = settings.model !== ''
|
|
251
|
+
? (() => {
|
|
252
|
+
const slash = settings.model.indexOf('/');
|
|
253
|
+
const provider = settings.model.slice(0, slash);
|
|
254
|
+
const model = settings.model.slice(slash + 1);
|
|
255
|
+
if (catalog.groups.some((group) => group.id === provider && group.models.some((entry) => entry.id === model))) return { provider, model };
|
|
256
|
+
throw Object.assign(new Error('the selected model is no longer listed'), { status: 409 });
|
|
257
|
+
})()
|
|
258
|
+
: catalog.default;
|
|
259
|
+
if (route === undefined) throw Object.assign(new Error('no model is selected'), { status: 409 });
|
|
260
|
+
const llm = get(ctx, 'llm');
|
|
261
|
+
if (llm?.prepareCall === undefined) throw new Error('the llm service is unavailable');
|
|
262
|
+
const timeout = AbortSignal.timeout(GENERATE_TIMEOUT_MS);
|
|
263
|
+
const callSignal = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
264
|
+
const call = await llm.prepareCall(route, callSignal, (controls, model) => {
|
|
265
|
+
const cap = Number.isSafeInteger(model?.defaultMaxTokens) && model.defaultMaxTokens > 0 ? Math.min(GENERATE_MAX_TOKENS, model.defaultMaxTokens) : GENERATE_MAX_TOKENS;
|
|
266
|
+
return { ...controls, maxTokens: cap };
|
|
267
|
+
});
|
|
268
|
+
let text = '';
|
|
269
|
+
let finish;
|
|
270
|
+
for await (const chunk of call.stream({ ...call.config, system: generateSystem(settings), signal: callSignal, messages: [{ role: 'user', content: [{ type: 'text', text: `<transcript>\n${transcript}\n</transcript>\n\nSuggest the followup prompts.` }] }] })) {
|
|
271
|
+
if (chunk.type === 'text-delta') text += chunk.text;
|
|
272
|
+
else if (chunk.type === 'finish') finish = chunk.reason;
|
|
273
|
+
}
|
|
274
|
+
if (finish?.kind === 'error' || finish?.kind === 'aborted') throw new Error(finish.failure?.message || `the model call ended: ${finish.kind}`);
|
|
275
|
+
return { followups: followupsFromModelText(text) ?? [], model: route.model };
|
|
276
|
+
}
|
|
277
|
+
|
|
159
278
|
function newerVersion(latest, current) {
|
|
160
279
|
for (let index = 0; index < 3; index += 1) {
|
|
161
280
|
if ((latest[index] ?? 0) !== (current[index] ?? 0)) return (latest[index] ?? 0) > (current[index] ?? 0);
|
|
@@ -181,20 +300,22 @@ async function checkUpdate(fetchImpl = fetch, pkg = PACKAGE, signal) {
|
|
|
181
300
|
|
|
182
301
|
async function apply(ctx) {
|
|
183
302
|
let settings = settingsLoad();
|
|
303
|
+
const json = (value, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } });
|
|
304
|
+
const isExternal = (next) => typeof next.model === 'string' && next.model !== '';
|
|
184
305
|
try {
|
|
185
|
-
ctx.tools.register(toolSpec(settings));
|
|
306
|
+
if (!isExternal(settings)) ctx.tools.register(toolSpec(settings));
|
|
186
307
|
} catch (error) {
|
|
187
308
|
console.error('dsh-nextsteps: tool registration failed:', error);
|
|
188
309
|
}
|
|
189
310
|
const reapplyTool = (next) => {
|
|
190
311
|
settings = next;
|
|
191
312
|
try {
|
|
192
|
-
ctx.tools.
|
|
313
|
+
if (isExternal(next)) ctx.tools.unregister(TOOL_NAME);
|
|
314
|
+
else ctx.tools.register(toolSpec(settings));
|
|
193
315
|
} catch (error) {
|
|
194
316
|
console.error('dsh-nextsteps: tool re-registration failed:', error);
|
|
195
317
|
}
|
|
196
318
|
};
|
|
197
|
-
const json = (value, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } });
|
|
198
319
|
ctx.connection?.fetch?.register({
|
|
199
320
|
path: '/api/dsh-nextsteps.settings',
|
|
200
321
|
methods: ['GET', 'POST'],
|
|
@@ -207,16 +328,50 @@ async function apply(ctx) {
|
|
|
207
328
|
const next = {
|
|
208
329
|
lang: ["auto","en","pt-BR","zh-CN","es","fr","de","it","ja","ko","ru","ar","hi","id","tr","vi"].includes(body?.lang) ? body.lang : current.lang,
|
|
209
330
|
maxFollowups: Number.isInteger(body?.maxFollowups) && body.maxFollowups >= 1 && body.maxFollowups <= 6 ? body.maxFollowups : current.maxFollowups,
|
|
210
|
-
showHeader: typeof body?.showHeader === 'boolean' ? body.showHeader : current.showHeader
|
|
331
|
+
showHeader: typeof body?.showHeader === 'boolean' ? body.showHeader : current.showHeader,
|
|
332
|
+
model: typeof body?.model === 'string' && body.model.includes('/') ? body.model : body?.model === '' || body?.model === undefined || body?.model === null ? '' : current.model
|
|
211
333
|
};
|
|
212
334
|
await settingsSave(next);
|
|
213
335
|
reapplyTool(next);
|
|
214
336
|
return json({ ok: true, settings: next });
|
|
215
337
|
}
|
|
216
338
|
});
|
|
339
|
+
ctx.connection?.fetch?.register({
|
|
340
|
+
path: '/api/dsh-nextsteps.models',
|
|
341
|
+
methods: ['GET'],
|
|
342
|
+
requestBody: 'buffered',
|
|
343
|
+
fetch: async (request) => {
|
|
344
|
+
try {
|
|
345
|
+
const catalog = await modelCatalog(ctx);
|
|
346
|
+
return json({ ok: true, ...catalog });
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return json({ error: String(error?.message ?? error) }, 502);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
ctx.connection?.fetch?.register({
|
|
353
|
+
path: '/api/dsh-nextsteps.generate',
|
|
354
|
+
methods: ['POST'],
|
|
355
|
+
requestBody: 'buffered',
|
|
356
|
+
fetch: async (request) => {
|
|
357
|
+
let body;
|
|
358
|
+
try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
|
|
359
|
+
if (typeof body?.sessionId !== 'string' || body.sessionId === '' || !Number.isSafeInteger(body?.afterSeq) || body.afterSeq < 0) {
|
|
360
|
+
return json({ error: 'bad request' }, 400);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const result = await generateForTurn(ctx, settings, body.sessionId, body.afterSeq, request.signal);
|
|
364
|
+
return json({ ok: true, rev: PLUGIN_REV, ...result });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const status = error?.status === 409 || error?.status === 400 ? error.status : 502;
|
|
367
|
+
return json({ error: String(error?.message ?? error) }, status);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
217
371
|
ctx.connection?.fetch?.register({
|
|
218
372
|
path: '/api/dsh-nextsteps.update',
|
|
219
373
|
methods: ['GET'],
|
|
374
|
+
requestBody: 'buffered',
|
|
220
375
|
fetch: async (request) => {
|
|
221
376
|
try {
|
|
222
377
|
const info = await checkUpdate(fetch, PACKAGE, request.signal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-nextsteps",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.js",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"lib/locale",
|
|
15
15
|
"docs/assets/icon.png",
|
|
16
16
|
"README.md",
|
|
17
|
-
"LICENSE"
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.pt-BR.md",
|
|
19
|
+
"README.zh-CN.md"
|
|
18
20
|
],
|
|
19
21
|
"dsh": {
|
|
20
22
|
"bundle": {
|