zigmini 0.9.0-beta.3 → 1.0.0-beta.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.ko.md +24 -4
- package/README.md +24 -4
- package/assets/agent/gateway-rules.md +1 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +51 -11
- package/docs/manual.md +52 -12
- package/host/omo-bridge/agent-control.ts +274 -0
- package/host/omo-bridge/bridge.ts +1 -1
- package/host/omo-bridge/continuity-control.ts +1 -1
- package/host/omo-bridge/task-control.ts +26 -16
- package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
- package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
- package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
- package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
- package/package.json +3 -2
- package/src/agent/api.ts +215 -0
- package/src/agent/definition.ts +177 -0
- package/src/agent/fields.ts +42 -0
- package/src/agent/files.ts +192 -0
- package/src/agent/legacy-files.ts +46 -0
- package/src/agent/list.ts +21 -0
- package/src/agent-rules.ts +14 -5
- package/src/approvals.ts +6 -1
- package/src/cli/main.ts +33 -0
- package/src/cli/messages.ts +10 -2
- package/src/cli/rotate-main.ts +2 -1
- package/src/cli/scan.ts +414 -0
- package/src/cli/security-import.ts +155 -0
- package/src/compact.ts +22 -0
- package/src/decisions.ts +6 -4
- package/src/engine/compat.json +3 -3
- package/src/engine/index.ts +1 -0
- package/src/engine/schema-guard.ts +12 -4
- package/src/engine/sdk.ts +8 -0
- package/src/formal-verification.ts +366 -0
- package/src/gc.ts +37 -6
- package/src/hub.ts +339 -79
- package/src/main.ts +95 -9
- package/src/maintenance.ts +1 -1
- package/src/manual.ts +9 -2
- package/src/profile-prompt.ts +73 -0
- package/src/profile.ts +33 -10
- package/src/protocol.ts +11 -2
- package/src/question-notifications.ts +4 -1
- package/src/reply-notifications.ts +7 -1
- package/src/repo-security-setting.ts +33 -0
- package/src/runtime/paths.ts +16 -0
- package/src/security/baseline.ts +170 -0
- package/src/security/dex-strings.ts +242 -0
- package/src/security/repo-scan.ts +369 -0
- package/src/security/revision-probe.ts +144 -0
- package/src/security/terms.ts +97 -0
- package/src/security-allow.ts +1 -0
- package/src/security-baseline.ts +113 -0
- package/src/security-check.ts +91 -16
- package/src/server-text.ts +38 -0
- package/src/store.ts +142 -16
- package/src/transcript.ts +7 -1
- package/src/ui-language.ts +33 -0
- package/src/update/check.ts +7 -3
- package/src/update/handoff-system.ts +24 -16
- package/src/update/handoff.ts +23 -12
- package/src/update/helper.ts +72 -36
- package/src/update/request.ts +46 -30
- package/src/update/state.ts +21 -11
- package/src/update/strategy.ts +12 -5
- package/src/update/target.ts +32 -15
- package/src/usage.ts +43 -20
- package/src/version.ts +39 -16
- package/src/voice/agents.ts +8 -3
- package/src/voice/install.ts +29 -18
- package/src/work-watch.ts +40 -18
- package/web/agent-icon.js +15 -9
- package/web/agents.d.ts +201 -0
- package/web/agents.js +813 -0
- package/web/app-download.js +32 -19
- package/web/app.js +1407 -278
- package/web/autocopy.js +6 -1
- package/web/boot.js +27 -8
- package/web/commands.js +20 -11
- package/web/decisions.js +70 -49
- package/web/devlog.js +25 -19
- package/web/diagram.js +8 -3
- package/web/enroll.js +43 -22
- package/web/gc.js +32 -19
- package/web/i18n/agent-icon.js +24 -0
- package/web/i18n/agents.js +48 -0
- package/web/i18n/app-download.js +26 -0
- package/web/i18n/autocopy.js +5 -0
- package/web/i18n/boot.js +24 -0
- package/web/i18n/commands.js +34 -0
- package/web/i18n/decisions.js +24 -0
- package/web/i18n/devlog.js +60 -0
- package/web/i18n/diagram.js +12 -0
- package/web/i18n/enroll.js +22 -0
- package/web/i18n/gc.js +38 -0
- package/web/i18n/lastsent.js +10 -0
- package/web/i18n/main.d.ts +4 -0
- package/web/i18n/main.js +527 -0
- package/web/i18n/manual.d.ts +4 -0
- package/web/i18n/manual.js +14 -0
- package/web/i18n/memo.js +30 -0
- package/web/i18n/notifications.js +16 -0
- package/web/i18n/profile.js +13 -0
- package/web/i18n/reference.js +26 -0
- package/web/i18n/security-inbox.js +40 -0
- package/web/i18n/slash.js +38 -0
- package/web/i18n/turn-stats.js +5 -0
- package/web/i18n/update.js +42 -0
- package/web/i18n/voice.js +28 -0
- package/web/i18n/wake.js +16 -0
- package/web/i18n/workid.js +10 -0
- package/web/i18n/workstatus.js +20 -0
- package/web/i18n.d.ts +20 -0
- package/web/i18n.js +63 -0
- package/web/index.html +156 -96
- package/web/lastsent.js +9 -4
- package/web/manual.d.ts +1 -1
- package/web/manual.js +11 -5
- package/web/memo.js +18 -13
- package/web/notifications.js +12 -7
- package/web/profile.js +42 -17
- package/web/reference.js +20 -15
- package/web/security-inbox.d.ts +18 -1
- package/web/security-inbox.js +173 -4
- package/web/slash.js +23 -13
- package/web/style.css +132 -3
- package/web/tooltip.js +5 -2
- package/web/turn-stats.js +6 -2
- package/web/update.js +31 -23
- package/web/voice.js +18 -8
- package/web/wake.d.ts +4 -0
- package/web/wake.js +7 -3
- package/web/workid.js +12 -2
- package/web/workstatus.js +36 -32
package/web/style.css
CHANGED
|
@@ -36,6 +36,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
36
36
|
.conv-list li.active { background: #404249; color: #fff; }
|
|
37
37
|
.conv-list li .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); margin-left: auto; display: none; }
|
|
38
38
|
.conv-list li.pending .dot { display: block; }
|
|
39
|
+
.main-list { margin-bottom: 12px; }
|
|
39
40
|
.sidebar-foot { background: #232428; padding: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; position: relative; }
|
|
40
41
|
.agent-avatar { cursor: pointer; }
|
|
41
42
|
.custom-icon { border-radius: 50%; object-fit: cover; }
|
|
@@ -220,7 +221,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
220
221
|
.voice-off #mic, .voice-off #toggle-wake, .voice-off #toggle-clap, .voice-off #toggle-tts { display: none; }
|
|
221
222
|
.device-requests:empty { display: none; }
|
|
222
223
|
.last-sent { flex: none; padding: 4px 16px; border-bottom: 1px solid var(--border); color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default; }
|
|
223
|
-
.last-sent::before { content:
|
|
224
|
+
.last-sent::before { content: attr(data-label); }
|
|
224
225
|
.last-sent.open { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 30vh; overflow-y: auto; }
|
|
225
226
|
.device-request { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #3a2f12; border-bottom: 1px solid var(--warn); font-size: 14px; }
|
|
226
227
|
.device-request span { flex: 1; }
|
|
@@ -583,6 +584,101 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
583
584
|
.app.status-open .ws-caption { font-size: 13px; padding: 4px 10px; }
|
|
584
585
|
}
|
|
585
586
|
|
|
587
|
+
/* 에이전트 (agent-sessions todo 10, design D5): the sidebar group above 대화, the chat header's name, stale badge and
|
|
588
|
+
편집, and the 에이전트 dialog (list, editor, 공통 기본값, 보관함) on the 설정 card. Tokens, 12px labels, 13px text,
|
|
589
|
+
4px/6px/8px radii and the #4e5058 tag outline are the existing ones. */
|
|
590
|
+
.agent-group { margin-bottom: 12px; }
|
|
591
|
+
.agent-manage { font-size: 16px; }
|
|
592
|
+
.agent-list li { gap: 8px; }
|
|
593
|
+
.agent-list li .agent-icon { width: 22px; height: 22px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--bg-input); font-size: 13px; line-height: 1; }
|
|
594
|
+
.agent-list li.active .agent-icon { background: #4e5058; }
|
|
595
|
+
.agent-list li .agent-conv-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
596
|
+
.agent-list li .agent-run { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--ok); box-shadow: 0 0 6px var(--ok); display: none; }
|
|
597
|
+
.agent-list li.running .agent-run { display: block; }
|
|
598
|
+
.agent-list li .agent-stale-dot { flex: none; font-size: 11px; color: var(--warn); display: none; }
|
|
599
|
+
.agent-list li.stale .agent-stale-dot { display: inline; }
|
|
600
|
+
.agent-list li .dot { margin-left: 0; }
|
|
601
|
+
.agent-list li.agent-empty, .agent-list li.agent-empty:hover { padding: 4px 8px 6px; font-size: 12px; color: var(--muted); background: none; cursor: default; }
|
|
602
|
+
.agent-stale:focus-visible, .pe-clear:focus-visible, .pe-list-row:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
|
|
603
|
+
.agent-head { display: flex; align-items: center; gap: 8px; min-width: 0; flex: none; }
|
|
604
|
+
.agent-head[hidden] { display: none; }
|
|
605
|
+
.agent-head-icon { width: 26px; height: 26px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--bg-input); font-size: 14px; line-height: 1; }
|
|
606
|
+
.agent-head-name { min-width: 0; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; }
|
|
607
|
+
.agent-stale { flex: none; border: 1px solid var(--warn); border-radius: 10px; background: rgba(240, 178, 50, 0.16); color: var(--warn); font: inherit; font-size: 12px; font-weight: 600; padding: 2px 8px; white-space: nowrap; }
|
|
608
|
+
.agent-stale:hover { background: var(--warn); color: #111; }
|
|
609
|
+
.agent-stale[hidden] { display: none; }
|
|
610
|
+
.agents-card { width: min(100%, 640px); height: min(100%, 860px); }
|
|
611
|
+
.agents-head { gap: 8px; }
|
|
612
|
+
.agents-nav { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
613
|
+
.agents-nav .text-btn[aria-current="true"] { background: var(--accent); color: #fff; }
|
|
614
|
+
.agents-status { margin: 0; padding: 8px 16px 0; font-size: 12px; color: var(--muted); }
|
|
615
|
+
.agents-status:empty { display: none; }
|
|
616
|
+
.agents-status.error { color: #f38b8d; }
|
|
617
|
+
.agents-status.ok { color: var(--ok); }
|
|
618
|
+
.agents-body { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
|
|
619
|
+
.pe-section { display: flex; flex-direction: column; gap: 8px; }
|
|
620
|
+
.pe-section + .pe-section { border-top: 1px solid var(--border); padding-top: 12px; }
|
|
621
|
+
.pe-section .section-label { padding: 0 0 2px; }
|
|
622
|
+
.pe-group { margin-top: 6px; justify-content: flex-start; flex-wrap: wrap; gap: 4px 8px; }
|
|
623
|
+
.pe-group > span:first-child { flex: none; }
|
|
624
|
+
.pe-group-help { font-weight: 400; text-transform: none; }
|
|
625
|
+
.pe-note { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
|
|
626
|
+
.pe-row { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: 4px 12px; align-items: start; }
|
|
627
|
+
.pe-label { font-size: 13px; padding-top: 6px; }
|
|
628
|
+
.pe-label small { display: block; font-size: 12px; color: var(--muted); font-weight: 400; }
|
|
629
|
+
.pe-control { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
630
|
+
.pe-control-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
|
|
631
|
+
.pe-input { flex: 1 1 160px; min-width: 0; padding: 6px 8px; font: inherit; font-size: 13px; color: var(--text); background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; }
|
|
632
|
+
.pe-input:focus-visible { outline: 2px solid #a3acff; outline-offset: 1px; }
|
|
633
|
+
.pe-input[aria-invalid="true"] { border-color: var(--danger); }
|
|
634
|
+
.pe-input.pe-number { flex: 0 1 140px; font-variant-numeric: tabular-nums; }
|
|
635
|
+
.pe-input.pe-select { flex: 0 1 180px; }
|
|
636
|
+
.pe-toggle { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; padding: 6px 0; cursor: pointer; }
|
|
637
|
+
.pe-toggle input { width: 16px; height: 16px; margin: 0; }
|
|
638
|
+
.pe-textarea { width: 100%; min-height: 72px; resize: vertical; padding: 8px; font: inherit; font-size: 13px; line-height: 1.5; color: var(--text); background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; }
|
|
639
|
+
.pe-textarea.pe-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; min-height: 96px; }
|
|
640
|
+
.pe-textarea.pe-body { min-height: 180px; }
|
|
641
|
+
.pe-origin { flex: none; font-size: 11px; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; white-space: nowrap; }
|
|
642
|
+
.pe-origin.agent, .pe-origin.defaults { color: #a3acff; border-color: #5865f2; }
|
|
643
|
+
.pe-clear { background: none; border: 0; padding: 2px 4px; border-radius: 4px; color: #00a8fc; font-size: 12px; }
|
|
644
|
+
.pe-clear:hover { text-decoration: underline; }
|
|
645
|
+
.pe-error { margin: 0; font-size: 12px; color: #f38b8d; }
|
|
646
|
+
.pe-error:empty { display: none; }
|
|
647
|
+
.pe-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
648
|
+
.pe-actions .pe-spacer { flex: 1; }
|
|
649
|
+
.pe-actions .text-btn.primary { background: var(--accent); color: #fff; }
|
|
650
|
+
.pe-actions .text-btn.primary:disabled, .pe-actions .text-btn.danger:disabled, .pe-actions .text-btn:disabled { opacity: 0.5; cursor: default; }
|
|
651
|
+
.text-btn.danger { background: var(--danger); color: #fff; }
|
|
652
|
+
.pe-delete { display: flex; flex-direction: column; gap: 8px; }
|
|
653
|
+
.pe-delete[hidden] { display: none; }
|
|
654
|
+
.pe-delete .pe-input { flex: none; width: 100%; max-width: 320px; }
|
|
655
|
+
.pe-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 8px; }
|
|
656
|
+
.pe-list-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--bg-card); border: 1px solid #4e5058; border-radius: 6px; }
|
|
657
|
+
.pe-list-row .agent-icon { width: 28px; height: 28px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--bg-input); font-size: 15px; line-height: 1; }
|
|
658
|
+
.pe-list-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
659
|
+
.pe-list-name { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 6px; }
|
|
660
|
+
.pe-list-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
661
|
+
.pe-list-actions { flex: none; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
|
|
662
|
+
.pe-empty { font-size: 13px; color: var(--muted); text-align: center; padding: 16px 8px; }
|
|
663
|
+
.pe-files { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
|
664
|
+
.pe-file { display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-size: 13px; border-radius: 4px; }
|
|
665
|
+
.pe-file:nth-child(odd) { background: var(--bg-card); }
|
|
666
|
+
.pe-file-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
667
|
+
.pe-file-name.directory { color: var(--muted); }
|
|
668
|
+
.pe-file-size { flex: none; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
669
|
+
.pe-upload { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
670
|
+
@media (max-width: 640px) {
|
|
671
|
+
.agent-head-name { max-width: 120px; }
|
|
672
|
+
.pe-row { grid-template-columns: 1fr; }
|
|
673
|
+
/* An agent conversation's header: the avatar and the agent block share the first row, the action buttons wrap below. */
|
|
674
|
+
.chat-head:has(.agent-head:not([hidden])) { flex-wrap: wrap; }
|
|
675
|
+
.agent-head { flex: 1 1 calc(100% - 44px); flex-wrap: wrap; }
|
|
676
|
+
.pe-label { padding-top: 0; }
|
|
677
|
+
.agents-body .text-btn, .agent-stale, .pe-clear { min-height: 44px; }
|
|
678
|
+
.pe-list-row { flex-wrap: wrap; }
|
|
679
|
+
.pe-list-actions { width: 100%; justify-content: flex-start; }
|
|
680
|
+
}
|
|
681
|
+
|
|
586
682
|
/* Bug 90: the page-drawn tooltip (web/tooltip.js). */
|
|
587
683
|
.zgw-tip { position: fixed; z-index: 10000; max-width: min(360px, calc(100vw - 16px)); padding: 6px 9px; border-radius: 6px; background: #111214; color: var(--text); border: 1px solid #3f4147; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); font-size: 12px; line-height: 1.45; white-space: pre-line; overflow-wrap: anywhere; pointer-events: none; }
|
|
588
684
|
.zgw-tip[hidden] { display: none; }
|
|
@@ -606,8 +702,41 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
606
702
|
.security-allowed-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 8px; }
|
|
607
703
|
.security-allowed-item { padding: 8px; border: 1px solid var(--border); border-radius: 4px; min-width: 0; }
|
|
608
704
|
.security-allowed-item .security-value { display: block; font-size: 12px; }
|
|
609
|
-
#security-allowed-confirm[hidden] { display: none; }
|
|
705
|
+
#security-allowed-confirm[hidden], #security-baseline-confirm[hidden] { display: none; }
|
|
706
|
+
.security-baseline-auto { border-style: dashed; }
|
|
707
|
+
/* Improvement 232 (todo 21): a disabled control in the section (locked list, blocked repository toggle) reads as disabled, like .pe-actions. */
|
|
708
|
+
#security-check .text-btn:disabled { opacity: 0.5; cursor: default; }
|
|
709
|
+
.formal-tools { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
710
|
+
.formal-tool { display: flex; flex-direction: column; gap: 4px; }
|
|
711
|
+
#formal-verification .text-btn:disabled { opacity: 0.5; cursor: default; }
|
|
712
|
+
/* Bug 259: a tool row's Install button (a compact .text-btn as tall as the switch) sits between the info icon and the
|
|
713
|
+
switch and never shrinks; the label beside it wraps, so the row fits a 360px wide screen. */
|
|
714
|
+
.formal-install { flex: none; padding: 2px 8px; }
|
|
715
|
+
/* Settings switches (improvement af188563): a role=switch button drawn as a track with a thumb, its label beside it, the
|
|
716
|
+
1-2 line summary under the row, and the info icon whose tooltip (web/tooltip.js) carries the full description. The
|
|
717
|
+
colours are the existing ones: the #4e5058 outline for off, --ok for on, the #a3acff focus ring. */
|
|
718
|
+
.switch-row { display: flex; align-items: center; gap: 8px; min-height: 28px; }
|
|
719
|
+
.switch-label { flex: 1; min-width: 0; font-size: 13px; line-height: 1.5; }
|
|
720
|
+
.switch-status { flex: none; font-size: 12px; color: var(--muted); }
|
|
721
|
+
.switch-status[hidden] { display: none; }
|
|
722
|
+
.switch-summary { margin: 0; }
|
|
723
|
+
.switch { position: relative; flex: none; width: 36px; height: 20px; padding: 0; border: 0; border-radius: 10px; background: #4e5058; cursor: pointer; transition: background 150ms ease; }
|
|
724
|
+
.switch::before { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform 150ms ease; }
|
|
725
|
+
.switch[aria-checked="true"] { background: var(--ok); }
|
|
726
|
+
.switch[aria-checked="true"]::before { transform: translateX(16px); }
|
|
727
|
+
.switch:disabled { opacity: 0.5; cursor: default; }
|
|
728
|
+
.info-tip { flex: none; display: inline-grid; place-items: center; width: 18px; height: 18px; padding: 0; border: 1px solid #4e5058; border-radius: 50%; background: none; color: var(--muted); font: 700 11px/1 ui-monospace, monospace; cursor: help; }
|
|
729
|
+
.info-tip:hover { color: var(--text); border-color: var(--text); }
|
|
730
|
+
.switch:focus-visible, .info-tip:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
|
|
731
|
+
@media (prefers-reduced-motion: reduce) {
|
|
732
|
+
.switch, .switch::before { transition: none; }
|
|
733
|
+
}
|
|
610
734
|
@media (max-width: 640px) {
|
|
611
|
-
.security-actions .text-btn, .security-inbox-item .text-btn, #security-check .text-btn { min-height: 44px; }
|
|
735
|
+
.security-actions .text-btn, .security-inbox-item .text-btn, #security-check .text-btn, #formal-verification .text-btn { min-height: 44px; }
|
|
736
|
+
/* The 44px hit areas of the switch and the info icon, without growing their drawings. */
|
|
737
|
+
.switch-row { min-height: 44px; }
|
|
738
|
+
.switch::after, .info-tip::after { content: ""; position: absolute; inset: -12px -4px; }
|
|
739
|
+
.info-tip { position: relative; }
|
|
740
|
+
.info-tip::after { inset: -13px; }
|
|
612
741
|
}
|
|
613
742
|
.security-card .security-value { white-space: pre-wrap; overflow-wrap: anywhere; }
|
package/web/tooltip.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Bug 90: the page draws its own tooltip. Some browsers (the user's, likely Aside) never show the native `title`
|
|
2
2
|
// tooltip, so a `title` moves to `data-tip` and one shared element shows it on hover and keyboard focus, linked with
|
|
3
|
-
// aria-describedby. Bug 161: only elements marked `data-tip-keep`
|
|
4
|
-
//
|
|
3
|
+
// aria-describedby. Bug 161: only elements marked `data-tip-keep` keep a visible tip; every other `title` moves to
|
|
4
|
+
// `aria-description`, so neither the page nor the browser shows a tip for it. The allowed set: 새 세션, the voice buttons,
|
|
5
|
+
// 확인함, 이벤트, and (improvement af188563) the `.info-tip` icons of the settings switches (message check, output check,
|
|
6
|
+
// formal verification and its per-tool rows), whose tip is the item's full description (`data-i18n-tip` in index.html,
|
|
7
|
+
// `data-tip` set by app.js for the tool rows).
|
|
5
8
|
|
|
6
9
|
export const TIP_GAP = 6;
|
|
7
10
|
export const TIP_MARGIN = 8;
|
package/web/turn-stats.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import strings from "./i18n/turn-stats.js";
|
|
2
|
+
import { registerStrings, t } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
registerStrings(strings);
|
|
1
5
|
/** Unknown fields are omitted, not rendered as zero. Model time excludes tools and wall time. */
|
|
2
6
|
export function turnStatsLabel(stats) {
|
|
3
7
|
if (!stats || typeof stats !== "object") return "";
|
|
4
8
|
const parts = [];
|
|
5
9
|
if (Number.isFinite(stats.tokensPerSecond) && stats.tokensPerSecond > 0) parts.push(`TPS ${stats.tokensPerSecond.toFixed(1)} tok/s`);
|
|
6
|
-
if (Number.isFinite(stats.modelDurationMs) && stats.modelDurationMs > 0) parts.push(
|
|
7
|
-
if (Number.isFinite(stats.cacheHitPercent) && stats.cacheHitPercent >= 0 && stats.cacheHitPercent <= 100) parts.push(
|
|
10
|
+
if (Number.isFinite(stats.modelDurationMs) && stats.modelDurationMs > 0) parts.push(t("turnStats.model", { seconds: (stats.modelDurationMs / 1000).toFixed(1) }));
|
|
11
|
+
if (Number.isFinite(stats.cacheHitPercent) && stats.cacheHitPercent >= 0 && stats.cacheHitPercent <= 100) parts.push(t("turnStats.cache", { percent: stats.cacheHitPercent.toFixed(1) }));
|
|
8
12
|
return parts.join(" · ");
|
|
9
13
|
}
|
|
10
14
|
|
package/web/update.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
// Plan Todo 22: the new version notice of the web UI (the header line and the
|
|
1
|
+
// Plan Todo 22: the new version notice of the web UI (the header line and the settings update area). Only decisions live
|
|
2
2
|
// here, with no DOM, so test/unit/update-web.test.ts and the mutation run drive them directly; app.js paints the result.
|
|
3
|
-
|
|
3
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
4
|
+
import strings from "./i18n/update.js";
|
|
5
|
+
|
|
6
|
+
registerStrings(strings);
|
|
4
7
|
|
|
5
8
|
export const UPDATE_STATUS_PATH = "/api/update/status";
|
|
6
9
|
export const UPDATE_CHECK_PATH = "/api/update/check";
|
|
7
10
|
export const UPDATE_PATH = "/api/update";
|
|
8
11
|
export const UPDATE_CONSENT_PATH = "/api/update/consent";
|
|
9
12
|
export const UPDATE_STATE_PATH = "/api/update/state";
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
|
|
13
|
+
export let NOT_CHECKED_LINE = t("update.notChecked");
|
|
14
|
+
export let UP_TO_DATE_LINE = t("update.upToDate");
|
|
15
|
+
export let CHECKOUT_LINE = t("update.checkout");
|
|
16
|
+
export let RESTART_NOTICE = t("update.restartNotice");
|
|
17
|
+
onLanguageChange(() => {
|
|
18
|
+
NOT_CHECKED_LINE = t("update.notChecked");
|
|
19
|
+
UP_TO_DATE_LINE = t("update.upToDate");
|
|
20
|
+
CHECKOUT_LINE = t("update.checkout");
|
|
21
|
+
RESTART_NOTICE = t("update.restartNotice");
|
|
22
|
+
});
|
|
15
23
|
|
|
16
24
|
/** A `version.available` payload or a GET /api/update/status body with a newer version; null for anything else. */
|
|
17
25
|
export function parseNotice(data) {
|
|
@@ -24,32 +32,32 @@ export function parseNotice(data) {
|
|
|
24
32
|
|
|
25
33
|
export function noticeLine(notice) {
|
|
26
34
|
if (notice === null) return null;
|
|
27
|
-
return
|
|
35
|
+
return t("update.noticeLine", { latest: notice.latest, current: notice.current });
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
/** The
|
|
38
|
+
/** The settings update area for a GET /api/update/status body (null when the gateway did not answer). */
|
|
31
39
|
export function updateView(status) {
|
|
32
40
|
if (status === null || typeof status !== "object" || typeof status.current !== "string") {
|
|
33
|
-
return { current: "
|
|
41
|
+
return { current: t("update.currentUnknown"), latest: t("update.notChecked"), note: null, changelogUrl: null, error: null, warning: null };
|
|
34
42
|
}
|
|
35
43
|
const notice = parseNotice(status);
|
|
36
|
-
const current =
|
|
37
|
-
const note = status.updatable === true ? null :
|
|
44
|
+
const current = t("update.current", { version: status.current });
|
|
45
|
+
const note = status.updatable === true ? null : t("update.checkout");
|
|
38
46
|
const failure = typeof status.error === "string" && status.error !== "" ? status.error : null;
|
|
39
|
-
const error = failure === null ? null :
|
|
47
|
+
const error = failure === null ? null : t("update.lastCheckFailed", { error: failure });
|
|
40
48
|
if (notice !== null) {
|
|
41
|
-
return { current, latest:
|
|
49
|
+
return { current, latest: t("update.latest", { version: notice.latest }), note, changelogUrl: notice.changelogUrl, error, warning: notice.updatable ? t("update.restartNotice") : null };
|
|
42
50
|
}
|
|
43
51
|
const checked = typeof status.checkedAt === "string" && typeof status.latest === "string";
|
|
44
52
|
if (!checked && failure !== null) {
|
|
45
|
-
return { current, latest:
|
|
53
|
+
return { current, latest: t("update.checkFailed", { error: failure }), note, changelogUrl: null, error: null, warning: null };
|
|
46
54
|
}
|
|
47
|
-
return { current, latest: checked ?
|
|
55
|
+
return { current, latest: checked ? t("update.upToDate") : t("update.notChecked"), note, changelogUrl: null, error, warning: null };
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/** The line when POST /api/update/check itself failed (status 0: the gateway did not answer). */
|
|
51
59
|
export function checkFailedLine(status) {
|
|
52
|
-
return
|
|
60
|
+
return t("update.checkFailed", { error: status === 0 ? t("update.gatewayNoResponse") : `HTTP ${status}` });
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// Plan Todo 23: the update button, its questions (new permission notice, shared schema, running turn) and its progress.
|
|
@@ -60,7 +68,7 @@ export function checkFailedLine(status) {
|
|
|
60
68
|
*/
|
|
61
69
|
export function questionGate(counters) {
|
|
62
70
|
const n = counters ? counters.pending + counters.needsReview : 0;
|
|
63
|
-
return n > 0 ?
|
|
71
|
+
return n > 0 ? t("update.questionsPending", { count: n }) : null;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
export function canUpdate(status) {
|
|
@@ -70,8 +78,8 @@ export function canUpdate(status) {
|
|
|
70
78
|
/** What the page does next with a POST /api/update answer. */
|
|
71
79
|
export function updateStep(status, body) {
|
|
72
80
|
const b = body !== null && typeof body === "object" ? body : {};
|
|
73
|
-
const message = typeof b.message === "string" ? b.message :
|
|
74
|
-
if (status === 202 && typeof b.id === "string") return { kind: "started", id: b.id, line:
|
|
81
|
+
const message = typeof b.message === "string" ? b.message : t("update.requestFailed", { status });
|
|
82
|
+
if (status === 202 && typeof b.id === "string") return { kind: "started", id: b.id, line: t("update.started", { from: b.from, to: b.to }) };
|
|
75
83
|
const notice = b.notice;
|
|
76
84
|
if (b.code === "consent_required" && notice !== null && typeof notice === "object" && Number.isInteger(notice.version) && typeof notice.text === "string") {
|
|
77
85
|
return { kind: "consent", message, notice: { version: notice.version, text: notice.text } };
|
|
@@ -85,8 +93,8 @@ export function updateStep(status, body) {
|
|
|
85
93
|
/** The line for an `update.result` payload. */
|
|
86
94
|
export function resultLine(result) {
|
|
87
95
|
if (result === null || typeof result !== "object" || typeof result.ok !== "boolean") return null;
|
|
88
|
-
if (result.ok) return
|
|
89
|
-
return `${typeof result.message === "string" ? result.message : "
|
|
96
|
+
if (result.ok) return t("update.completed", { from: result.from, to: result.to });
|
|
97
|
+
return `${typeof result.message === "string" ? result.message : t("update.failed")}${typeof result.logPath === "string" && result.logPath !== "" ? ` ${t("update.logPath", { path: result.logPath })}` : ""}`;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
/**
|
|
@@ -101,5 +109,5 @@ export function progressLine(data, loadedVersion = null) {
|
|
|
101
109
|
return resultLine(data.result);
|
|
102
110
|
}
|
|
103
111
|
if (typeof data.label !== "string" || data.phase === "done") return null;
|
|
104
|
-
return
|
|
112
|
+
return t("update.progress", { from: data.from, to: data.to, label: data.label });
|
|
105
113
|
}
|
package/web/voice.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// servers). Only decisions live here, with no DOM, so test/unit/voice-web.test.ts and the mutation run can exercise
|
|
3
3
|
// them directly. Contract: .omo/evidence/improvement-71/design.md, "Clients".
|
|
4
4
|
|
|
5
|
+
import dict from "./i18n/voice.js";
|
|
6
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
7
|
+
|
|
8
|
+
registerStrings(dict);
|
|
9
|
+
|
|
5
10
|
export const VOICE_SETTING_PATH = "/api/settings/voice";
|
|
6
11
|
/** Todo 26: an npm install's voice install (GET status, POST start; the POST works on the web port only). */
|
|
7
12
|
export const VOICE_INSTALL_PATH = "/api/settings/voice/install";
|
|
@@ -10,8 +15,13 @@ export const VOICE_OFF_CLASS = "voice-off";
|
|
|
10
15
|
export const VOICE_CONTROL_IDS = Object.freeze(["mic", "toggle-wake", "toggle-clap", "toggle-tts"]);
|
|
11
16
|
/** The web slash commands that need the voice servers: out of the menu and refused while voice is off. */
|
|
12
17
|
export const VOICE_COMMANDS = Object.freeze(["tts on", "tts off", "wake on", "wake off", "clap on", "clap off", "read"]);
|
|
13
|
-
|
|
14
|
-
export
|
|
18
|
+
/** Live bindings: they follow the UI language, so importers always read the current text. */
|
|
19
|
+
export let VOICE_DISABLED_LINE = t("voice.disabledLine");
|
|
20
|
+
export let VOICE_OFF_STATUS_LINE = t("voice.offStatusLine");
|
|
21
|
+
onLanguageChange(() => {
|
|
22
|
+
VOICE_DISABLED_LINE = t("voice.disabledLine");
|
|
23
|
+
VOICE_OFF_STATUS_LINE = t("voice.offStatusLine");
|
|
24
|
+
});
|
|
15
25
|
export const TTS_KEY = "zgw.tts.enabled";
|
|
16
26
|
export const CLAP_KEY = "zgw.clap.enabled";
|
|
17
27
|
|
|
@@ -50,7 +60,7 @@ export function restorePlan(storage, micAvailable) {
|
|
|
50
60
|
|
|
51
61
|
/** The header toggle's text and pressed state. */
|
|
52
62
|
export function voiceToggleView(enabled) {
|
|
53
|
-
return { text: enabled ? "
|
|
63
|
+
return { text: enabled ? t("voice.toggle.on") : t("voice.toggle.off"), pressed: enabled };
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/** The PUT that changes the setting (web port only; the gateway answers {"enabled":...} and publishes voice.updated). */
|
|
@@ -64,9 +74,9 @@ export function voiceInstallView(status) {
|
|
|
64
74
|
return { hidden: true, text: "", disabled: true };
|
|
65
75
|
}
|
|
66
76
|
const progress = status.progress ?? {};
|
|
67
|
-
if (progress.state === "running") return { hidden: false, text:
|
|
68
|
-
const size =
|
|
69
|
-
return { hidden: false, text: progress.state === "failed" ?
|
|
77
|
+
if (progress.state === "running") return { hidden: false, text: t("voice.install.running", { index: progress.index, total: progress.total, message: progress.message }), disabled: true };
|
|
78
|
+
const size = t("voice.install.size", { gb: Math.round(status.estimateBytes / 1024 ** 3) });
|
|
79
|
+
return { hidden: false, text: t(progress.state === "failed" ? "voice.install.retry" : "voice.install.start", { size }), disabled: false };
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/** A voice.install.progress payload applied to the last known status; "done" marks voice as installed. */
|
|
@@ -78,6 +88,6 @@ export function installStatusWith(status, progress) {
|
|
|
78
88
|
/** The /옵션 line. With voice off the three features are reported as unavailable instead of their stored states. */
|
|
79
89
|
export function optionStatusLine(voiceEnabled, { tts, wake, clap }) {
|
|
80
90
|
if (!voiceEnabled) return VOICE_OFF_STATUS_LINE;
|
|
81
|
-
const onOff = (on) => (on ? "
|
|
82
|
-
return
|
|
91
|
+
const onOff = (on) => t(on ? "voice.state.on" : "voice.state.off");
|
|
92
|
+
return t("voice.status.line", { tts: onOff(tts), wake: onOff(wake), clap: onOff(clap) });
|
|
83
93
|
}
|
package/web/wake.d.ts
ADDED
package/web/wake.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import dict from "./i18n/wake.js";
|
|
2
|
+
|
|
3
|
+
const words = (key) => [...new Set([dict.en[key], dict.ko[key]].flatMap((value) => value.split(",")))].join("|");
|
|
4
|
+
|
|
5
|
+
const WAKE_RE = new RegExp(`(?:^|[\\s,.!?])(?:${words("wake.hey")})[\\s,.!?~-]*(?:${words("wake.name")})[\\s,.!?~-]*`, "i");
|
|
2
6
|
const WAKE_MAX_OFFSET = 6;
|
|
3
7
|
|
|
4
8
|
export function matchWake(text) {
|
|
@@ -7,8 +11,8 @@ export function matchWake(text) {
|
|
|
7
11
|
return { command: text.slice(m.index + m[0].length).trim() };
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
const TRIPLE_HEY_RE =
|
|
11
|
-
const CANCEL_RE =
|
|
14
|
+
const TRIPLE_HEY_RE = new RegExp(`^[\\s,.!?~-]*(?:(?:${words("wake.hey.triple")})[\\s,.!?~-]*){3,}$`, "i");
|
|
15
|
+
const CANCEL_RE = new RegExp(`^[\\s,.!?~-]*(?:${words("wake.cancel")})[\\s,.!?~-]*$`);
|
|
12
16
|
|
|
13
17
|
export function isTripleHey(text) {
|
|
14
18
|
return TRIPLE_HEY_RE.test(text);
|
package/web/workid.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import dict from "./i18n/workid.js";
|
|
2
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
3
|
+
|
|
1
4
|
// Improvement 76: a right-click (or the ContextMenu key / Shift+F10) on a work card in the work status drawing copies the
|
|
2
5
|
// card's full gateway work id. Only work cards react; every other element keeps the browser's own menu.
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
registerStrings(dict);
|
|
8
|
+
|
|
9
|
+
/** Live bindings: they follow the UI language, so importers always read the current text. */
|
|
10
|
+
export let ID_COPIED = t("workid.copied");
|
|
11
|
+
export let ID_COPY_FAILED = t("workid.copyFailed");
|
|
12
|
+
onLanguageChange(() => {
|
|
13
|
+
ID_COPIED = t("workid.copied");
|
|
14
|
+
ID_COPY_FAILED = t("workid.copyFailed");
|
|
15
|
+
});
|
|
6
16
|
const WORK_PREFIX = "work:";
|
|
7
17
|
|
|
8
18
|
/** The full work id of the work card that holds [target] (`g.ws-node` with `data-key="work:<id>"`), or null. */
|
package/web/workstatus.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// What the gateway is working on (state.snapshot `workStatus` + `work.status` frames): DTO validation, socket generation
|
|
2
2
|
// and revision gating, the view and the hub diagram drawn inside the sidebar region above the profile block.
|
|
3
|
+
import strings from "./i18n/workstatus.js";
|
|
4
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
5
|
+
|
|
6
|
+
registerStrings(strings);
|
|
3
7
|
|
|
4
8
|
/** Work cards beside the fixed main card (gateway WORK_SLOTS). */
|
|
5
9
|
export const SLOT_COUNT = 6;
|
|
@@ -17,30 +21,30 @@ export function writeFolded(storage, folded) {
|
|
|
17
21
|
|
|
18
22
|
/** The fold button's accessible name: what activating it does. */
|
|
19
23
|
export function foldLabel(folded) {
|
|
20
|
-
return folded ? "
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
waiting_input: "입력 대기",
|
|
26
|
-
waiting: "대기 중",
|
|
27
|
-
unresponsive: "응답 없음",
|
|
28
|
-
broken: "고장",
|
|
29
|
-
unknown: "확인 중",
|
|
30
|
-
idle: "쉬는 중",
|
|
31
|
-
completed: "완료",
|
|
32
|
-
failed: "실패",
|
|
33
|
-
cancelled: "취소됨",
|
|
34
|
-
};
|
|
24
|
+
return t(folded ? "workstatus.fold.expand" : "workstatus.fold.hide");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const STATES = ["running", "waiting_input", "waiting", "unresponsive", "broken", "unknown", "idle", "completed", "failed", "cancelled"];
|
|
28
|
+
export let STATE_LABELS = stateLabels();
|
|
35
29
|
/** A work waiting between turns on its sub-tasks (gateway `waitingFor: "children"`). */
|
|
36
|
-
export
|
|
30
|
+
export let CHILD_WAIT_LABEL = t("workstatus.state.children");
|
|
31
|
+
|
|
32
|
+
function stateLabels() {
|
|
33
|
+
return Object.fromEntries(STATES.map((state) => [state, t(`workstatus.state.${state}`)]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The exported labels follow the UI language.
|
|
37
|
+
onLanguageChange(() => {
|
|
38
|
+
STATE_LABELS = stateLabels();
|
|
39
|
+
CHILD_WAIT_LABEL = t("workstatus.state.children");
|
|
40
|
+
});
|
|
37
41
|
const SESSION_STATES = new Set(["running", "idle", "unknown"]);
|
|
38
42
|
const WORK_STATES = new Set(["running", "waiting", "waiting_input", "unresponsive", "broken", "completed", "failed", "cancelled", "unknown"]);
|
|
39
43
|
/** Finished works and works the gateway's watcher judged broken are the only ones the user can clear from their slot. */
|
|
40
44
|
const FINISHED = new Set(["completed", "failed", "cancelled", "broken"]);
|
|
41
45
|
/** The watcher's reason line is short by contract; anything longer is cut here. */
|
|
42
46
|
const NOTE_MAX = 160;
|
|
43
|
-
const KIND_LABELS = { session: "
|
|
47
|
+
const KIND_LABELS = { session: "workstatus.kind.session", work: "workstatus.kind.work" };
|
|
44
48
|
|
|
45
49
|
function record(value) {
|
|
46
50
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -119,10 +123,10 @@ export function relativeTime(iso, now) {
|
|
|
119
123
|
const at = Date.parse(iso);
|
|
120
124
|
if (Number.isNaN(at)) return "";
|
|
121
125
|
const seconds = Math.max(0, Math.floor((now - at) / 1000));
|
|
122
|
-
if (seconds < 60) return "
|
|
123
|
-
if (seconds < 3600) return
|
|
124
|
-
if (seconds < 86400) return
|
|
125
|
-
return
|
|
126
|
+
if (seconds < 60) return t("workstatus.time.now");
|
|
127
|
+
if (seconds < 3600) return t("workstatus.time.minutes", { count: Math.floor(seconds / 60) });
|
|
128
|
+
if (seconds < 86400) return t("workstatus.time.hours", { count: Math.floor(seconds / 3600) });
|
|
129
|
+
return t("workstatus.time.days", { count: Math.floor(seconds / 86400) });
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
/**
|
|
@@ -140,7 +144,7 @@ export function workStatusView(status, conversationTitles, now) {
|
|
|
140
144
|
key: `${item.kind}:${item.id}`,
|
|
141
145
|
id: item.id,
|
|
142
146
|
kind: item.kind,
|
|
143
|
-
kindLabel: KIND_LABELS[item.kind],
|
|
147
|
+
kindLabel: t(KIND_LABELS[item.kind]),
|
|
144
148
|
title: item.title,
|
|
145
149
|
state: item.state,
|
|
146
150
|
stateLabel: item.waitingFor === "children" ? CHILD_WAIT_LABEL : STATE_LABELS[item.state],
|
|
@@ -191,8 +195,8 @@ export function incomingReports(previous, view) {
|
|
|
191
195
|
|
|
192
196
|
/** The text under the diagram: the state of the region, or the latest change. */
|
|
193
197
|
export function captionText(view) {
|
|
194
|
-
if (view.mode === "unavailable") return "
|
|
195
|
-
if (view.mode === "empty" || (view.slots.every((slot) => slot === null) && view.running === 0)) return "
|
|
198
|
+
if (view.mode === "unavailable") return t("workstatus.caption.unavailable");
|
|
199
|
+
if (view.mode === "empty" || (view.slots.every((slot) => slot === null) && view.running === 0)) return t("workstatus.caption.empty");
|
|
196
200
|
return `${view.caption.title} · ${view.caption.stateLabel} · ${view.caption.when}`;
|
|
197
201
|
}
|
|
198
202
|
|
|
@@ -480,8 +484,8 @@ export function renderWorkStatus(parts, view, previous, motion) {
|
|
|
480
484
|
for (let i = 0; i < 9; i++) {
|
|
481
485
|
pubG.append(svg("circle", { cx: pub.x + 9 + (i % 3) * 5, cy: pub.y + 11 + Math.floor(i / 3) * 6, r: 1.3, class: i % 4 === 0 ? "ws-grid-on" : "ws-grid" }));
|
|
482
486
|
}
|
|
483
|
-
pubG.append(svg("text", { x: pub.x + 30, y: pub.y + 16, class: "ws-pub-name" }, fitLabel(parts.userName ?? "
|
|
484
|
-
pubG.append(svg("text", { x: pub.x + 30, y: pub.y + 28, class: "ws-pub-state" }, "
|
|
487
|
+
pubG.append(svg("text", { x: pub.x + 30, y: pub.y + 16, class: "ws-pub-name" }, fitLabel(parts.userName ?? t("workstatus.user.me"), 7.6)));
|
|
488
|
+
pubG.append(svg("text", { x: pub.x + 30, y: pub.y + 28, class: "ws-pub-state" }, t("workstatus.user")));
|
|
485
489
|
nodes.append(pubG);
|
|
486
490
|
|
|
487
491
|
const { hub } = layout;
|
|
@@ -500,7 +504,7 @@ export function renderWorkStatus(parts, view, previous, motion) {
|
|
|
500
504
|
if (slot.empty) {
|
|
501
505
|
const g = svg("g", { class: "ws-slot-empty", "data-slot": slot.index, "aria-hidden": "true" });
|
|
502
506
|
g.append(svg("rect", { x: slot.x + 0.5, y: slot.y + 0.5, width: slot.w - 1, height: slot.h - 1, rx: 4, class: "ws-empty-box" }));
|
|
503
|
-
g.append(svg("text", { x: slot.x + slot.w / 2, y: slot.y + slot.h / 2 + 4, class: "ws-empty-label" }, "
|
|
507
|
+
g.append(svg("text", { x: slot.x + slot.w / 2, y: slot.y + slot.h / 2 + 4, class: "ws-empty-label" }, t("workstatus.slot.empty")));
|
|
504
508
|
nodes.append(g);
|
|
505
509
|
continue;
|
|
506
510
|
}
|
|
@@ -508,7 +512,7 @@ export function renderWorkStatus(parts, view, previous, motion) {
|
|
|
508
512
|
links.append(svg("path", { d: node.link, class: `ws-link ws-${node.state}`, style: `--hue:${node.hue}` }));
|
|
509
513
|
hubG.append(svg("circle", { cx: node.anchor.x.toFixed(1), cy: node.anchor.y.toFixed(1), r: 2.4, class: "ws-port", style: `--hue:${node.hue}` }));
|
|
510
514
|
const described = `${node.title}, ${node.stateLabel}${node.note ? ` (${node.note})` : ""}, ${node.when}`;
|
|
511
|
-
const label = node.finished ? `${described}.
|
|
515
|
+
const label = node.finished ? `${described}. ${t("workstatus.slot.clear")}` : described;
|
|
512
516
|
const g = svg("g", {
|
|
513
517
|
class: `ws-node ws-${node.state}${node.finished ? " ws-dismissable" : ""}`, style: `--hue:${node.hue}`, "data-key": node.key, "data-slot": node.index,
|
|
514
518
|
tabindex: "0", role: node.finished ? "button" : "img", "aria-label": label,
|
|
@@ -518,7 +522,7 @@ export function renderWorkStatus(parts, view, previous, motion) {
|
|
|
518
522
|
node.lines.forEach((line, i) => g.append(svg("text", { x: node.x + 9, y: node.y + 21 + i * 16, class: "ws-node-label" }, line)));
|
|
519
523
|
g.append(svg("circle", { cx: node.x + 12, cy: node.y + 57, r: 3, class: "ws-node-dot" }));
|
|
520
524
|
g.append(svg("text", { x: node.x + 20, y: node.y + 61, class: "ws-node-state" }, node.stateLabel));
|
|
521
|
-
g.append(svg("text", { x: node.x + 9, y: node.y + 80, class: "ws-node-when" }, node.finished ? `${node.when} ·
|
|
525
|
+
g.append(svg("text", { x: node.x + 9, y: node.y + 80, class: "ws-node-when" }, node.finished ? `${node.when} · ${t("workstatus.slot.clear.short")}` : node.when));
|
|
522
526
|
g.addEventListener("pointerenter", () => showTip(tip, region, g, node.title, node.note));
|
|
523
527
|
g.addEventListener("pointerleave", () => hideTip(tip));
|
|
524
528
|
g.addEventListener("focus", () => showTip(tip, region, g, node.title, node.note));
|
|
@@ -572,10 +576,10 @@ export function renderWorkStatus(parts, view, previous, motion) {
|
|
|
572
576
|
caption.textContent = text;
|
|
573
577
|
caption.title = text;
|
|
574
578
|
more.hidden = view.queued === 0;
|
|
575
|
-
more.textContent = view.queued > 0 ?
|
|
576
|
-
more.title = view.queued > 0 ?
|
|
579
|
+
more.textContent = view.queued > 0 ? t("workstatus.queued.badge", { count: view.queued }) : "";
|
|
580
|
+
more.title = view.queued > 0 ? t("workstatus.queued.title", { count: view.queued }) : "";
|
|
577
581
|
const rows = [view.publisher, ...view.slots.filter(Boolean)].filter(Boolean);
|
|
578
582
|
list.replaceChildren(...rows.map((row) => el("li", "", `${row.kindLabel} ${row.title}: ${row.stateLabel}${row.note ? ` (${row.note})` : ""}, ${row.when}${row.owner ? ` (${row.owner})` : ""}`)));
|
|
579
|
-
if (view.queued > 0) list.append(el("li", "",
|
|
583
|
+
if (view.queued > 0) list.append(el("li", "", t("workstatus.queued.item", { count: view.queued })));
|
|
580
584
|
return signatures;
|
|
581
585
|
}
|