cli-jaw 1.7.33 → 1.7.34

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 (77) hide show
  1. package/dist/bin/commands/dispatch.js +8 -0
  2. package/dist/bin/commands/dispatch.js.map +1 -1
  3. package/dist/bin/commands/memory.js +7 -1
  4. package/dist/bin/commands/memory.js.map +1 -1
  5. package/dist/bin/commands/orchestrate.js +67 -8
  6. package/dist/bin/commands/orchestrate.js.map +1 -1
  7. package/dist/src/agent/args.js +4 -0
  8. package/dist/src/agent/args.js.map +1 -1
  9. package/dist/src/agent/events.js +50 -20
  10. package/dist/src/agent/events.js.map +1 -1
  11. package/dist/src/agent/opencode-diagnostics.js +106 -0
  12. package/dist/src/agent/opencode-diagnostics.js.map +1 -0
  13. package/dist/src/agent/spawn-env.js +75 -4
  14. package/dist/src/agent/spawn-env.js.map +1 -1
  15. package/dist/src/agent/spawn.js +104 -15
  16. package/dist/src/agent/spawn.js.map +1 -1
  17. package/dist/src/cli/commands.js +1 -1
  18. package/dist/src/cli/commands.js.map +1 -1
  19. package/dist/src/cli/handlers-runtime.js +23 -5
  20. package/dist/src/cli/handlers-runtime.js.map +1 -1
  21. package/dist/src/core/compact.js +8 -7
  22. package/dist/src/core/compact.js.map +1 -1
  23. package/dist/src/core/runtime-settings-gate.js +40 -0
  24. package/dist/src/core/runtime-settings-gate.js.map +1 -0
  25. package/dist/src/core/runtime-settings.js +71 -64
  26. package/dist/src/core/runtime-settings.js.map +1 -1
  27. package/dist/src/orchestrator/pipeline.js +20 -0
  28. package/dist/src/orchestrator/pipeline.js.map +1 -1
  29. package/dist/src/orchestrator/state-machine.js +8 -5
  30. package/dist/src/orchestrator/state-machine.js.map +1 -1
  31. package/dist/src/prompt/templates/a1-system.md +9 -1
  32. package/dist/src/prompt/templates/employee.md +5 -1
  33. package/dist/src/routes/orchestrate.js +52 -11
  34. package/dist/src/routes/orchestrate.js.map +1 -1
  35. package/package.json +6 -5
  36. package/public/css/modals.css +126 -0
  37. package/public/dist/assets/{employees-Do9d6Xi5.js → employees-p53cgGmH.js} +1 -1
  38. package/public/dist/assets/{index-qALA03H1.css → index-CLKLbGzn.css} +1 -1
  39. package/public/dist/assets/index-wUWc2M5K.js +32 -0
  40. package/public/dist/assets/memory-6zLEr-qI.js +1 -0
  41. package/public/dist/assets/{memory-DeZSzBAb.js → memory-C2i7ZIvv.js} +2 -2
  42. package/public/dist/assets/{render-CQnnZ-_i.js → render-CulTuvJs.js} +1 -1
  43. package/public/dist/assets/settings-BUEiZgkm.js +40 -0
  44. package/public/dist/assets/settings-BhrOslae.js +1 -0
  45. package/public/dist/assets/{skills-Ci5t_dsV.js → skills-CSuSbBWa.js} +1 -1
  46. package/public/dist/assets/skills-CgwxEvFx.js +1 -0
  47. package/public/dist/assets/slash-commands-Bo8jvBfI.js +1 -0
  48. package/public/dist/assets/{slash-commands-0RvnZU9z.js → slash-commands-D-v0DlbY.js} +1 -1
  49. package/public/dist/assets/ui-4JiRyxJy.js +131 -0
  50. package/public/dist/assets/ui-Dx0MwI23.js +1 -0
  51. package/public/dist/assets/ws-DKtFfZsY.js +14 -0
  52. package/public/dist/index.html +74 -15
  53. package/public/index.html +72 -13
  54. package/public/js/features/attention-badge.ts +151 -0
  55. package/public/js/features/chat.ts +16 -0
  56. package/public/js/features/help-content.ts +75 -0
  57. package/public/js/features/help-dialog.ts +164 -0
  58. package/public/js/features/memory.ts +2 -2
  59. package/public/js/features/orchestrate-scope.ts +4 -0
  60. package/public/js/features/settings-core.ts +36 -11
  61. package/public/js/main.ts +4 -0
  62. package/public/js/ui.ts +21 -1
  63. package/public/js/virtual-scroll.ts +72 -8
  64. package/public/js/ws.ts +50 -6
  65. package/public/locales/en.json +183 -2
  66. package/public/locales/ko.json +183 -2
  67. package/scripts/smoke/opencode-external-dir-smoke.ts +350 -0
  68. package/public/dist/assets/index-yGExjgR_.js +0 -32
  69. package/public/dist/assets/memory-Dpe-qPbZ.js +0 -1
  70. package/public/dist/assets/settings-C8bSXG3q.js +0 -40
  71. package/public/dist/assets/settings-COrhSfDh.js +0 -1
  72. package/public/dist/assets/skills-BO0V4aHG.js +0 -1
  73. package/public/dist/assets/slash-commands-DbUvFtCk.js +0 -1
  74. package/public/dist/assets/ui-Cxk1_e0b.js +0 -1
  75. package/public/dist/assets/ui-IWxpAzJ7.js +0 -131
  76. package/public/dist/assets/ws-FsYmCE65.js +0 -14
  77. /package/public/dist/assets/{constants-IeOVgtYz.js → constants-BU8a_R5s.js} +0 -0
@@ -25,9 +25,9 @@
25
25
  href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap"
26
26
  rel="stylesheet">
27
27
  <!-- Vite handles module bundling in dev (HMR) and production (build) -->
28
- <script type="module" crossorigin src="/dist/assets/index-yGExjgR_.js"></script>
28
+ <script type="module" crossorigin src="/dist/assets/index-wUWc2M5K.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
30
- <link rel="stylesheet" crossorigin href="/dist/assets/index-qALA03H1.css">
30
+ <link rel="stylesheet" crossorigin href="/dist/assets/index-CLKLbGzn.css">
31
31
  </head>
32
32
 
33
33
  <body>
@@ -43,7 +43,11 @@
43
43
  </div>
44
44
 
45
45
  <div>
46
- <div class="section-title" data-i18n="sidebar.memory">메모리</div>
46
+ <div class="section-title-row">
47
+ <div class="section-title" data-i18n="sidebar.memory">메모리</div>
48
+ <button class="help-trigger" type="button" data-help-topic="memory"
49
+ data-i18n-aria="help.memory.aria" aria-label="메모리 도움말">?</button>
50
+ </div>
47
51
  <button class="sidebar-hb-btn" id="memorySidebarBtn" aria-label="Open memory panel"><span data-icon="brain"></span> Memory (0)</button>
48
52
  </div>
49
53
 
@@ -118,7 +122,11 @@
118
122
  </div>
119
123
  <button class="sidebar-hb-btn" id="btnClearChat">/clear</button>
120
124
  <button class="sidebar-hb-btn" id="hbSidebarBtn"><span data-icon="heartPulse"></span> Heartbeat (0)</button>
121
- <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
125
+ <div class="sidebar-action-row">
126
+ <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
127
+ <button class="help-trigger" type="button" data-help-topic="promptTemplates"
128
+ data-i18n-aria="help.promptTemplates.aria" aria-label="프롬프트 템플릿 도움말">?</button>
129
+ </div>
122
130
  <button class="sidebar-hb-btn" id="langToggle" title="한국어 ⇄ English"><span data-icon="web"></span> 한국어</button>
123
131
  </div>
124
132
  </nav>
@@ -197,7 +205,11 @@
197
205
  <!-- Agents Tab -->
198
206
  <div id="tabAgents" class="tab-content active" role="tabpanel" aria-labelledby="tabBtnAgents">
199
207
  <div>
200
- <label data-i18n="label.activeCli">활성 CLI</label>
208
+ <label class="label-with-help">
209
+ <span data-i18n="label.activeCli">활성 CLI</span>
210
+ <button class="help-trigger" type="button" data-help-topic="activeCli"
211
+ data-i18n-aria="help.activeCli.aria" aria-label="활성 CLI 도움말">?</button>
212
+ </label>
201
213
  <select id="selCli">
202
214
  <option value="claude">Claude</option>
203
215
  <option value="codex">Codex</option>
@@ -208,11 +220,19 @@
208
220
  </div>
209
221
 
210
222
  <div>
211
- <label data-i18n="label.model">모델</label>
223
+ <label class="label-with-help">
224
+ <span data-i18n="label.model">모델</span>
225
+ <button class="help-trigger" type="button" data-help-topic="model"
226
+ data-i18n-aria="help.model.aria" aria-label="모델 도움말">?</button>
227
+ </label>
212
228
  <select id="selModel"></select>
213
229
  </div>
214
230
  <div>
215
- <label data-i18n="label.effort">추론 강도</label>
231
+ <label class="label-with-help">
232
+ <span data-i18n="label.effort">추론 강도</span>
233
+ <button class="help-trigger" type="button" data-help-topic="effort"
234
+ data-i18n-aria="help.effort.aria" aria-label="추론 강도 도움말">?</button>
235
+ </label>
216
236
  <select id="selEffort">
217
237
  <option value="">— none</option>
218
238
  <option value="low">🟢 low</option>
@@ -222,7 +242,11 @@
222
242
  </div>
223
243
 
224
244
  <div>
225
- <label data-i18n="label.permissions">권한</label>
245
+ <label class="label-with-help">
246
+ <span data-i18n="label.permissions">권한</span>
247
+ <button class="help-trigger" type="button" data-help-topic="permissions"
248
+ data-i18n-aria="help.permissions.aria" aria-label="권한 도움말">?</button>
249
+ </label>
226
250
  <div class="perm-toggle">
227
251
  <span class="perm-btn active perm-auto" ><span data-icon="exec"></span> Auto</span>
228
252
  </div>
@@ -238,6 +262,8 @@
238
262
  <details id="flushAgentDetails" class="flush-agent-section">
239
263
  <summary class="section-title" style="cursor:pointer; user-select:none;">
240
264
  <span data-icon="lightbulb"></span> Flush Agent
265
+ <button class="help-trigger" type="button" data-help-topic="flushAgent"
266
+ data-i18n-aria="help.flushAgent.aria" aria-label="Flush Agent 도움말">?</button>
241
267
  <span id="flushAgentBadge" class="text-xs text-dim" style="margin-left:4px"></span>
242
268
  </summary>
243
269
  <div class="flush-agent-fields" style="margin-top:6px">
@@ -264,7 +290,11 @@
264
290
  <hr class="hr-divider">
265
291
 
266
292
  <div class="flex justify-between items-center">
267
- <div class="section-title" data-i18n="sidebar.employees">직원</div>
293
+ <div class="label-with-help">
294
+ <div class="section-title" data-i18n="sidebar.employees">직원</div>
295
+ <button class="help-trigger" type="button" data-help-topic="employees"
296
+ data-i18n-aria="help.employees.aria" aria-label="직원 도움말">?</button>
297
+ </div>
268
298
  <button class="btn-clear btn-action-sm" data-action="addEmployee"
269
299
  data-i18n="btn.addEmployee">+ 추가</button>
270
300
  </div>
@@ -275,6 +305,11 @@
275
305
 
276
306
  <!-- Skills Tab -->
277
307
  <div id="tabSkills" class="tab-content" role="tabpanel" aria-labelledby="tabBtnSkills">
308
+ <div class="section-title-row">
309
+ <div class="section-title" data-i18n="tab.skills">스킬</div>
310
+ <button class="help-trigger" type="button" data-help-topic="skills"
311
+ data-i18n-aria="help.skills.aria" aria-label="스킬 도움말">?</button>
312
+ </div>
278
313
  <div class="flex gap-2 flex-wrap">
279
314
  <button class="skill-filter active" data-filter="all" data-i18n="skill.filter.all">전체</button>
280
315
  <button class="skill-filter" data-filter="installed" data-i18n="skill.filter.installed"><span data-icon="compacting"></span> 설치됨</button>
@@ -299,7 +334,11 @@
299
334
  시스템 프롬프트 편집</button>
300
335
  <!-- Active Channel Toggle -->
301
336
  <div class="settings-group">
302
- <h4><span data-icon="radio"></span> Active Channel</h4>
337
+ <h4 class="section-title-row">
338
+ <span><span data-icon="radio"></span> Active Channel</span>
339
+ <button class="help-trigger" type="button" data-help-topic="activeChannel"
340
+ data-i18n-aria="help.activeChannel.aria" aria-label="Active Channel 도움말">?</button>
341
+ </h4>
303
342
  <div class="settings-row">
304
343
  <label>Runtime</label>
305
344
  <div>
@@ -311,7 +350,11 @@
311
350
 
312
351
  <!-- Telegram Bot -->
313
352
  <div id="channelTelegramSettings" class="settings-group">
314
- <h4><span data-provider="telegram"></span> Telegram</h4>
353
+ <h4 class="section-title-row">
354
+ <span><span data-provider="telegram"></span> Telegram</span>
355
+ <button class="help-trigger" type="button" data-help-topic="telegram"
356
+ data-i18n-aria="help.telegram.aria" aria-label="Telegram 도움말">?</button>
357
+ </h4>
315
358
  <div class="settings-row">
316
359
  <label data-i18n="label.enabled">활성화</label>
317
360
  <div>
@@ -346,7 +389,11 @@
346
389
 
347
390
  <!-- Discord Bot -->
348
391
  <div id="channelDiscordSettings" class="settings-group" style="display:none">
349
- <h4><span data-provider="discord"></span> Discord</h4>
392
+ <h4 class="section-title-row">
393
+ <span><span data-provider="discord"></span> Discord</span>
394
+ <button class="help-trigger" type="button" data-help-topic="discord"
395
+ data-i18n-aria="help.discord.aria" aria-label="Discord 도움말">?</button>
396
+ </h4>
350
397
  <div class="settings-row">
351
398
  <label data-i18n="label.enabled">활성화</label>
352
399
  <div>
@@ -508,7 +555,11 @@
508
555
 
509
556
  <!-- Fallback Order -->
510
557
  <div class="settings-group">
511
- <h4><span data-icon="exec"></span> Fallback</h4>
558
+ <h4 class="section-title-row">
559
+ <span><span data-icon="exec"></span> Fallback</span>
560
+ <button class="help-trigger" type="button" data-help-topic="fallbackOrder"
561
+ data-i18n-aria="help.fallbackOrder.aria" aria-label="Fallback 도움말">?</button>
562
+ </h4>
512
563
  <p class="text-xs text-dim fallback-desc" data-i18n="fallback.desc">CLI 실패 시 자동
513
564
  재시도 순서</p>
514
565
  <div id="fallbackOrderList"></div>
@@ -516,7 +567,11 @@
516
567
 
517
568
  <!-- MCP Servers -->
518
569
  <div class="settings-group">
519
- <h4><span data-icon="link"></span> MCP Servers</h4>
570
+ <h4 class="section-title-row">
571
+ <span><span data-icon="link"></span> MCP Servers</span>
572
+ <button class="help-trigger" type="button" data-help-topic="mcp"
573
+ data-i18n-aria="help.mcp.aria" aria-label="MCP 도움말">?</button>
574
+ </h4>
520
575
  <div id="mcpServerList" class="text-status py-1">
521
576
  Loading...
522
577
  </div>
@@ -529,7 +584,11 @@
529
584
 
530
585
  <!-- STT Settings -->
531
586
  <div class="settings-group">
532
- <h4 data-i18n="stt.title"><span data-icon="mic"></span> 음성 인식 (STT)</h4>
587
+ <h4 class="section-title-row">
588
+ <span><span data-icon="mic"></span> <span data-i18n="stt.title">음성 인식 (STT)</span></span>
589
+ <button class="help-trigger" type="button" data-help-topic="stt"
590
+ data-i18n-aria="help.stt.aria" aria-label="STT 도움말">?</button>
591
+ </h4>
533
592
  <div class="settings-row">
534
593
  <label data-i18n="stt.engine">엔진</label>
535
594
  <select id="sttEngine">
package/public/index.html CHANGED
@@ -49,7 +49,11 @@
49
49
  </div>
50
50
 
51
51
  <div>
52
- <div class="section-title" data-i18n="sidebar.memory">메모리</div>
52
+ <div class="section-title-row">
53
+ <div class="section-title" data-i18n="sidebar.memory">메모리</div>
54
+ <button class="help-trigger" type="button" data-help-topic="memory"
55
+ data-i18n-aria="help.memory.aria" aria-label="메모리 도움말">?</button>
56
+ </div>
53
57
  <button class="sidebar-hb-btn" id="memorySidebarBtn" aria-label="Open memory panel"><span data-icon="brain"></span> Memory (0)</button>
54
58
  </div>
55
59
 
@@ -124,7 +128,11 @@
124
128
  </div>
125
129
  <button class="sidebar-hb-btn" id="btnClearChat">/clear</button>
126
130
  <button class="sidebar-hb-btn" id="hbSidebarBtn"><span data-icon="heartPulse"></span> Heartbeat (0)</button>
127
- <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
131
+ <div class="sidebar-action-row">
132
+ <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
133
+ <button class="help-trigger" type="button" data-help-topic="promptTemplates"
134
+ data-i18n-aria="help.promptTemplates.aria" aria-label="프롬프트 템플릿 도움말">?</button>
135
+ </div>
128
136
  <button class="sidebar-hb-btn" id="langToggle" title="한국어 ⇄ English"><span data-icon="web"></span> 한국어</button>
129
137
  </div>
130
138
  </nav>
@@ -203,7 +211,11 @@
203
211
  <!-- Agents Tab -->
204
212
  <div id="tabAgents" class="tab-content active" role="tabpanel" aria-labelledby="tabBtnAgents">
205
213
  <div>
206
- <label data-i18n="label.activeCli">활성 CLI</label>
214
+ <label class="label-with-help">
215
+ <span data-i18n="label.activeCli">활성 CLI</span>
216
+ <button class="help-trigger" type="button" data-help-topic="activeCli"
217
+ data-i18n-aria="help.activeCli.aria" aria-label="활성 CLI 도움말">?</button>
218
+ </label>
207
219
  <select id="selCli">
208
220
  <option value="claude">Claude</option>
209
221
  <option value="codex">Codex</option>
@@ -214,11 +226,19 @@
214
226
  </div>
215
227
 
216
228
  <div>
217
- <label data-i18n="label.model">모델</label>
229
+ <label class="label-with-help">
230
+ <span data-i18n="label.model">모델</span>
231
+ <button class="help-trigger" type="button" data-help-topic="model"
232
+ data-i18n-aria="help.model.aria" aria-label="모델 도움말">?</button>
233
+ </label>
218
234
  <select id="selModel"></select>
219
235
  </div>
220
236
  <div>
221
- <label data-i18n="label.effort">추론 강도</label>
237
+ <label class="label-with-help">
238
+ <span data-i18n="label.effort">추론 강도</span>
239
+ <button class="help-trigger" type="button" data-help-topic="effort"
240
+ data-i18n-aria="help.effort.aria" aria-label="추론 강도 도움말">?</button>
241
+ </label>
222
242
  <select id="selEffort">
223
243
  <option value="">— none</option>
224
244
  <option value="low">🟢 low</option>
@@ -228,7 +248,11 @@
228
248
  </div>
229
249
 
230
250
  <div>
231
- <label data-i18n="label.permissions">권한</label>
251
+ <label class="label-with-help">
252
+ <span data-i18n="label.permissions">권한</span>
253
+ <button class="help-trigger" type="button" data-help-topic="permissions"
254
+ data-i18n-aria="help.permissions.aria" aria-label="권한 도움말">?</button>
255
+ </label>
232
256
  <div class="perm-toggle">
233
257
  <span class="perm-btn active perm-auto" ><span data-icon="exec"></span> Auto</span>
234
258
  </div>
@@ -244,6 +268,8 @@
244
268
  <details id="flushAgentDetails" class="flush-agent-section">
245
269
  <summary class="section-title" style="cursor:pointer; user-select:none;">
246
270
  <span data-icon="lightbulb"></span> Flush Agent
271
+ <button class="help-trigger" type="button" data-help-topic="flushAgent"
272
+ data-i18n-aria="help.flushAgent.aria" aria-label="Flush Agent 도움말">?</button>
247
273
  <span id="flushAgentBadge" class="text-xs text-dim" style="margin-left:4px"></span>
248
274
  </summary>
249
275
  <div class="flush-agent-fields" style="margin-top:6px">
@@ -270,7 +296,11 @@
270
296
  <hr class="hr-divider">
271
297
 
272
298
  <div class="flex justify-between items-center">
273
- <div class="section-title" data-i18n="sidebar.employees">직원</div>
299
+ <div class="label-with-help">
300
+ <div class="section-title" data-i18n="sidebar.employees">직원</div>
301
+ <button class="help-trigger" type="button" data-help-topic="employees"
302
+ data-i18n-aria="help.employees.aria" aria-label="직원 도움말">?</button>
303
+ </div>
274
304
  <button class="btn-clear btn-action-sm" data-action="addEmployee"
275
305
  data-i18n="btn.addEmployee">+ 추가</button>
276
306
  </div>
@@ -281,6 +311,11 @@
281
311
 
282
312
  <!-- Skills Tab -->
283
313
  <div id="tabSkills" class="tab-content" role="tabpanel" aria-labelledby="tabBtnSkills">
314
+ <div class="section-title-row">
315
+ <div class="section-title" data-i18n="tab.skills">스킬</div>
316
+ <button class="help-trigger" type="button" data-help-topic="skills"
317
+ data-i18n-aria="help.skills.aria" aria-label="스킬 도움말">?</button>
318
+ </div>
284
319
  <div class="flex gap-2 flex-wrap">
285
320
  <button class="skill-filter active" data-filter="all" data-i18n="skill.filter.all">전체</button>
286
321
  <button class="skill-filter" data-filter="installed" data-i18n="skill.filter.installed"><span data-icon="compacting"></span> 설치됨</button>
@@ -305,7 +340,11 @@
305
340
  시스템 프롬프트 편집</button>
306
341
  <!-- Active Channel Toggle -->
307
342
  <div class="settings-group">
308
- <h4><span data-icon="radio"></span> Active Channel</h4>
343
+ <h4 class="section-title-row">
344
+ <span><span data-icon="radio"></span> Active Channel</span>
345
+ <button class="help-trigger" type="button" data-help-topic="activeChannel"
346
+ data-i18n-aria="help.activeChannel.aria" aria-label="Active Channel 도움말">?</button>
347
+ </h4>
309
348
  <div class="settings-row">
310
349
  <label>Runtime</label>
311
350
  <div>
@@ -317,7 +356,11 @@
317
356
 
318
357
  <!-- Telegram Bot -->
319
358
  <div id="channelTelegramSettings" class="settings-group">
320
- <h4><span data-provider="telegram"></span> Telegram</h4>
359
+ <h4 class="section-title-row">
360
+ <span><span data-provider="telegram"></span> Telegram</span>
361
+ <button class="help-trigger" type="button" data-help-topic="telegram"
362
+ data-i18n-aria="help.telegram.aria" aria-label="Telegram 도움말">?</button>
363
+ </h4>
321
364
  <div class="settings-row">
322
365
  <label data-i18n="label.enabled">활성화</label>
323
366
  <div>
@@ -352,7 +395,11 @@
352
395
 
353
396
  <!-- Discord Bot -->
354
397
  <div id="channelDiscordSettings" class="settings-group" style="display:none">
355
- <h4><span data-provider="discord"></span> Discord</h4>
398
+ <h4 class="section-title-row">
399
+ <span><span data-provider="discord"></span> Discord</span>
400
+ <button class="help-trigger" type="button" data-help-topic="discord"
401
+ data-i18n-aria="help.discord.aria" aria-label="Discord 도움말">?</button>
402
+ </h4>
356
403
  <div class="settings-row">
357
404
  <label data-i18n="label.enabled">활성화</label>
358
405
  <div>
@@ -514,7 +561,11 @@
514
561
 
515
562
  <!-- Fallback Order -->
516
563
  <div class="settings-group">
517
- <h4><span data-icon="exec"></span> Fallback</h4>
564
+ <h4 class="section-title-row">
565
+ <span><span data-icon="exec"></span> Fallback</span>
566
+ <button class="help-trigger" type="button" data-help-topic="fallbackOrder"
567
+ data-i18n-aria="help.fallbackOrder.aria" aria-label="Fallback 도움말">?</button>
568
+ </h4>
518
569
  <p class="text-xs text-dim fallback-desc" data-i18n="fallback.desc">CLI 실패 시 자동
519
570
  재시도 순서</p>
520
571
  <div id="fallbackOrderList"></div>
@@ -522,7 +573,11 @@
522
573
 
523
574
  <!-- MCP Servers -->
524
575
  <div class="settings-group">
525
- <h4><span data-icon="link"></span> MCP Servers</h4>
576
+ <h4 class="section-title-row">
577
+ <span><span data-icon="link"></span> MCP Servers</span>
578
+ <button class="help-trigger" type="button" data-help-topic="mcp"
579
+ data-i18n-aria="help.mcp.aria" aria-label="MCP 도움말">?</button>
580
+ </h4>
526
581
  <div id="mcpServerList" class="text-status py-1">
527
582
  Loading...
528
583
  </div>
@@ -535,7 +590,11 @@
535
590
 
536
591
  <!-- STT Settings -->
537
592
  <div class="settings-group">
538
- <h4 data-i18n="stt.title"><span data-icon="mic"></span> 음성 인식 (STT)</h4>
593
+ <h4 class="section-title-row">
594
+ <span><span data-icon="mic"></span> <span data-i18n="stt.title">음성 인식 (STT)</span></span>
595
+ <button class="help-trigger" type="button" data-help-topic="stt"
596
+ data-i18n-aria="help.stt.aria" aria-label="STT 도움말">?</button>
597
+ </h4>
539
598
  <div class="settings-row">
540
599
  <label data-i18n="stt.engine">엔진</label>
541
600
  <select id="sttEngine">
@@ -0,0 +1,151 @@
1
+ type BadgeNavigator = Navigator & {
2
+ setAppBadge?: (contents?: number) => Promise<void>;
3
+ clearAppBadge?: () => Promise<void>;
4
+ };
5
+
6
+ const COMPLETION_DEDUPE_MS = 800;
7
+ const BADGE_SIZE = 64;
8
+ const BASE_TITLE_FALLBACK = 'CLI-JAW';
9
+
10
+ let initialized = false;
11
+ let unreadCount = 0;
12
+ let baseTitle = BASE_TITLE_FALLBACK;
13
+ let faviconLink: HTMLLinkElement | null = null;
14
+ let originalFaviconHref = '';
15
+ let createdFaviconLink = false;
16
+ let lastNotifyAt = 0;
17
+
18
+ function getBadgeNavigator(): BadgeNavigator {
19
+ return navigator as BadgeNavigator;
20
+ }
21
+
22
+ function shouldCountUnread(): boolean {
23
+ return document.visibilityState !== 'visible' || !document.hasFocus();
24
+ }
25
+
26
+ function isDuplicateCompletion(now: number): boolean {
27
+ return now - lastNotifyAt < COMPLETION_DEDUPE_MS;
28
+ }
29
+
30
+ function getOrCreateFaviconLink(): HTMLLinkElement | null {
31
+ if (faviconLink) return faviconLink;
32
+ const existing = document.querySelector<HTMLLinkElement>('link[rel~="icon"]');
33
+ if (existing) {
34
+ faviconLink = existing;
35
+ originalFaviconHref = existing.href || existing.getAttribute('href') || '';
36
+ return existing;
37
+ }
38
+ const link = document.createElement('link');
39
+ link.rel = 'icon';
40
+ document.head.appendChild(link);
41
+ faviconLink = link;
42
+ createdFaviconLink = true;
43
+ return link;
44
+ }
45
+
46
+ function renderBadgeFavicon(count: number): string {
47
+ const canvas = document.createElement('canvas');
48
+ canvas.width = BADGE_SIZE;
49
+ canvas.height = BADGE_SIZE;
50
+ const ctx = canvas.getContext('2d');
51
+ if (!ctx) return originalFaviconHref;
52
+
53
+ ctx.clearRect(0, 0, BADGE_SIZE, BADGE_SIZE);
54
+ ctx.font = '44px "Apple Color Emoji", "Segoe UI Emoji", sans-serif';
55
+ ctx.textAlign = 'center';
56
+ ctx.textBaseline = 'middle';
57
+ ctx.fillText('🦈', 28, 36);
58
+
59
+ ctx.beginPath();
60
+ ctx.arc(50, 15, count > 1 ? 12 : 9, 0, Math.PI * 2);
61
+ ctx.fillStyle = '#ff335f';
62
+ ctx.fill();
63
+ ctx.lineWidth = 4;
64
+ ctx.strokeStyle = '#ffffff';
65
+ ctx.stroke();
66
+
67
+ if (count > 1) {
68
+ ctx.fillStyle = '#ffffff';
69
+ ctx.font = 'bold 15px sans-serif';
70
+ ctx.textAlign = 'center';
71
+ ctx.textBaseline = 'middle';
72
+ ctx.fillText(String(Math.min(count, 9)), 50, 16);
73
+ }
74
+
75
+ return canvas.toDataURL('image/png');
76
+ }
77
+
78
+ async function setAppBadgeBestEffort(count: number): Promise<void> {
79
+ const nav = getBadgeNavigator();
80
+ if (typeof nav.setAppBadge !== 'function') return;
81
+ try {
82
+ await nav.setAppBadge(count);
83
+ } catch {
84
+ // Browser support varies; title/favicon remain the reliable baseline.
85
+ }
86
+ }
87
+
88
+ async function clearAppBadgeBestEffort(): Promise<void> {
89
+ const nav = getBadgeNavigator();
90
+ if (typeof nav.clearAppBadge !== 'function') return;
91
+ try {
92
+ await nav.clearAppBadge();
93
+ } catch {
94
+ // Best-effort only.
95
+ }
96
+ }
97
+
98
+ function applyUnreadState(): void {
99
+ document.title = `(${unreadCount}) ${baseTitle}`;
100
+ const link = getOrCreateFaviconLink();
101
+ if (link) link.href = renderBadgeFavicon(unreadCount);
102
+ void setAppBadgeBestEffort(unreadCount);
103
+ }
104
+
105
+ function restoreTitle(): void {
106
+ document.title = baseTitle;
107
+ }
108
+
109
+ function restoreFavicon(): void {
110
+ if (!faviconLink) return;
111
+ if (createdFaviconLink) {
112
+ faviconLink.remove();
113
+ faviconLink = null;
114
+ createdFaviconLink = false;
115
+ return;
116
+ }
117
+ faviconLink.href = originalFaviconHref;
118
+ }
119
+
120
+ export function initAttentionBadge(): void {
121
+ if (initialized) return;
122
+ initialized = true;
123
+ baseTitle = document.title || BASE_TITLE_FALLBACK;
124
+ getOrCreateFaviconLink();
125
+ document.addEventListener('visibilitychange', () => {
126
+ if (document.visibilityState === 'visible') clearUnreadResponses();
127
+ });
128
+ window.addEventListener('focus', () => clearUnreadResponses());
129
+ }
130
+
131
+ export function notifyUnreadResponse(): void {
132
+ if (!initialized || !shouldCountUnread()) return;
133
+ const now = Date.now();
134
+ if (isDuplicateCompletion(now)) return;
135
+ lastNotifyAt = now;
136
+ unreadCount += 1;
137
+ applyUnreadState();
138
+ }
139
+
140
+ export function clearUnreadResponses(): void {
141
+ if (!initialized || unreadCount === 0) return;
142
+ unreadCount = 0;
143
+ lastNotifyAt = 0;
144
+ restoreTitle();
145
+ restoreFavicon();
146
+ void clearAppBadgeBestEffort();
147
+ }
148
+
149
+ export function getUnreadResponseCount(): number {
150
+ return unreadCount;
151
+ }
@@ -9,6 +9,9 @@ import { escapeHtml, cancelPostRender } from '../render.js';
9
9
  import { getVirtualScroll } from '../virtual-scroll.js';
10
10
  import { clearCache, upsertMessage } from './idb-cache.js';
11
11
  import { ICONS } from '../icons.js';
12
+ import { clearUnreadResponses } from './attention-badge.js';
13
+ import { syncOrchestrateSnapshot } from '../ws.js';
14
+ import { waitForSettingsSaveIdle } from './settings-core.js';
12
15
 
13
16
  let activeObjectURLs: string[] = [];
14
17
 
@@ -20,6 +23,10 @@ function getCommandTimeoutMs(text: string): number {
20
23
  return /^\/compact(?:\s|$)/i.test(String(text || '').trim()) ? 5 * 60 * 1000 : 10_000;
21
24
  }
22
25
 
26
+ function isOrchestrateCommand(text: string): boolean {
27
+ return /^\/(?:orchestrate|pabcd)(?:\s|$)/i.test(String(text || '').trim());
28
+ }
29
+
23
30
  // In-flight guard: prevents double-send from rapid clicks / Enter-bursts while the
24
31
  // POST to /api/message is outstanding. Server-side dedup in gateway.ts is the
25
32
  // second line of defense. See devlog/_plan/260417_message_duplication/.
@@ -43,6 +50,7 @@ export async function sendMessage(): Promise<void> {
43
50
 
44
51
  const text = input.value.trim();
45
52
  if (!text && !state.attachedFiles.length) return;
53
+ clearUnreadResponses();
46
54
 
47
55
  // Mark in-flight AND disable send button for visual feedback.
48
56
  __chatSending = true;
@@ -50,12 +58,15 @@ export async function sendMessage(): Promise<void> {
50
58
  const prevDisabled = sendBtn.disabled;
51
59
  sendBtn.disabled = true;
52
60
  try {
61
+ await waitForSettingsSaveIdle();
62
+
53
63
  // File paths like /Users/junny/... or /tmp/foo — not commands
54
64
  const afterSlash = text.slice(1).trim();
55
65
  const firstToken = afterSlash.split(/\s+/)[0] || '';
56
66
  const isFilePath = firstToken.includes('/') || firstToken.includes('\\');
57
67
 
58
68
  if (text.startsWith('/') && !state.attachedFiles.length && !isFilePath) {
69
+ const shouldSyncOrchestrate = isOrchestrateCommand(text);
59
70
  input.value = '';
60
71
  resetInputHeight();
61
72
  slashCmd.close();
@@ -100,6 +111,10 @@ export async function sendMessage(): Promise<void> {
100
111
  if (result?.text) addSystemMsg(escapeHtml(result.text), '', result.type);
101
112
  } catch (err) {
102
113
  addSystemMsg(t('chat.cmd.fail', { msg: (err as Error).message }), '', 'error');
114
+ } finally {
115
+ if (shouldSyncOrchestrate) {
116
+ syncOrchestrateSnapshot('command').catch(() => {});
117
+ }
103
118
  }
104
119
  return;
105
120
  }
@@ -244,6 +259,7 @@ export async function clearChat(): Promise<void> {
244
259
  const { cleanupToolActivity } = await import('../ui.js');
245
260
  cleanupToolActivity();
246
261
  clearCache().catch(() => {});
262
+ clearUnreadResponses();
247
263
  }
248
264
 
249
265
  // ── Auto-resize textarea (RAF-batched to avoid blocking input) ──