zigmini 0.8.0-beta.5 → 0.8.0-beta.7
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 +78 -19
- package/README.md +79 -19
- package/assets/agent/gateway-request-rules.md +1 -0
- package/assets/agent/gateway-rules.md +6 -0
- 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 +25 -2
- package/docs/manual.md +25 -2
- package/host/omo-bridge/task-control.ts +4 -0
- package/host/omo-check/patches/omo-5.1.35-compaction-idle.patch +77 -0
- package/host/omo-check/patches/omo-5.1.35-compaction-intent.patch +212 -0
- package/host/omo-check/patches/omo-5.1.35-compaction-model.patch +299 -0
- package/host/omo-check/patches/omo-5.1.35-compaction-preservation.patch +1063 -0
- package/host/omo-check/patches/omo-5.1.35-compaction-refusal.patch +83 -0
- package/host/omo-check/patches/omo-5.1.35-gateway-port.manifest.json +171 -8
- package/host/omo-check/patches/omo-5.1.35-phantom-attach.patch +31 -0
- package/host/omo-check/patches/omo-5.1.35-request-rules.patch +109 -0
- package/host/omo-check/patches/omo-5.1.35-turn-stats.patch +76 -0
- package/package.json +4 -1
- package/scripts/secret-rules.ts +101 -0
- package/scripts/secret-scan.allow.json +155 -0
- package/scripts/secret-scan.rules.json +30 -0
- package/src/agent-request-rules.ts +82 -0
- package/src/cli/access.ts +31 -34
- package/src/cli/launchd.ts +30 -4
- package/src/cli/main.ts +13 -39
- package/src/cli/messages.ts +160 -0
- package/src/cli/pair.ts +32 -25
- package/src/cli/rotate-main.ts +411 -0
- package/src/cli/setup.ts +164 -75
- package/src/decisions.ts +20 -1
- package/src/hub.ts +140 -30
- package/src/main.ts +45 -4
- package/src/omo/client.ts +28 -0
- package/src/protocol.ts +33 -1
- package/src/reply-notifications.ts +144 -0
- package/src/runtime/paths.ts +2 -0
- package/src/security-check.ts +241 -0
- package/src/store.ts +71 -1
- package/src/turn-stats.ts +49 -0
- package/src/work-watch.ts +36 -10
- package/src/work.ts +29 -4
- package/web/app.js +197 -10
- package/web/decisions.js +3 -0
- package/web/gc.d.ts +2 -0
- package/web/gc.js +11 -0
- package/web/index.html +26 -10
- package/web/notifications.d.ts +25 -0
- package/web/notifications.js +118 -0
- package/web/style.css +16 -0
- package/web/tooltip.d.ts +0 -6
- package/web/tooltip.js +8 -11
- package/web/turn-stats.d.ts +26 -0
- package/web/turn-stats.js +58 -0
- package/web/update.d.ts +1 -1
- package/web/update.js +10 -3
package/web/style.css
CHANGED
|
@@ -126,6 +126,8 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
126
126
|
.msg-name { font-weight: 600; }
|
|
127
127
|
.msg-name.bot { color: #a3acff; }
|
|
128
128
|
.msg-time { font-size: 12px; color: var(--muted); }
|
|
129
|
+
.turn-stats { margin-top: 4px; margin-right: 48px; font-size: 12px; line-height: 1.5; color: var(--muted); font-variant-numeric: tabular-nums; user-select: none; }
|
|
130
|
+
.turn-stats::before { content: attr(data-label); }
|
|
129
131
|
.msg-tag { font-size: 11px; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 4px; }
|
|
130
132
|
.msg-read { font-size: 11px; color: var(--muted); }
|
|
131
133
|
.msg.precomment .msg-body { opacity: 0.72; font-size: 0.9em; }
|
|
@@ -412,6 +414,10 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
412
414
|
/* 정리 (improvement 66): inside .settings-body after 앱 내려받기; contain keeps its nowrap rows from widening the card on phones. */
|
|
413
415
|
.settings-section.gc { contain: inline-size; }
|
|
414
416
|
.gc-summary { text-transform: none; color: var(--text); }
|
|
417
|
+
.gc-head { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
418
|
+
.gc-all { display: inline-flex; align-items: center; gap: 6px; text-transform: none; color: var(--text); font-weight: 400; cursor: pointer; white-space: nowrap; }
|
|
419
|
+
.gc-all[hidden] { display: none; }
|
|
420
|
+
.gc-all input { width: 16px; height: 16px; margin: 0; }
|
|
415
421
|
.gc-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
|
|
416
422
|
.gc-list:empty { display: none; }
|
|
417
423
|
.gc-row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg-card); border: 1px solid #4e5058; border-radius: 6px; font-size: 13px; }
|
|
@@ -565,3 +571,13 @@ button { font: inherit; color: inherit; cursor: pointer; }
|
|
|
565
571
|
/* Bug 90: the page-drawn tooltip (web/tooltip.js). */
|
|
566
572
|
.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; }
|
|
567
573
|
.zgw-tip[hidden] { display: none; }
|
|
574
|
+
|
|
575
|
+
/* Improvement 165: the security check card (values the user decides to send or drop). */
|
|
576
|
+
.security-card { display: block; margin: 8px 0; border: 1px solid #4e5058; border-left: 4px solid var(--warn); background: var(--bg-card); border-radius: 6px; padding: 12px; max-width: 640px; }
|
|
577
|
+
.security-card .security-title { font-weight: 600; margin-bottom: 6px; }
|
|
578
|
+
.security-card .security-list { margin: 0 0 8px; padding-left: 18px; }
|
|
579
|
+
.security-card .security-value { word-break: break-all; }
|
|
580
|
+
.security-card .security-place { opacity: 0.7; font-size: 0.9em; }
|
|
581
|
+
.security-card .security-actions { display: flex; gap: 8px; }
|
|
582
|
+
.security-card .security-actions .btn { white-space: nowrap; writing-mode: horizontal-tb; }
|
|
583
|
+
.security-card.done { opacity: 0.75; }
|
package/web/tooltip.d.ts
CHANGED
|
@@ -4,17 +4,11 @@ export type TipElement = {
|
|
|
4
4
|
setAttribute(name: string, value: string): void;
|
|
5
5
|
removeAttribute(name: string): void;
|
|
6
6
|
hasAttribute(name: string): boolean;
|
|
7
|
-
textContent: string | null;
|
|
8
|
-
scrollWidth: number;
|
|
9
|
-
clientWidth: number;
|
|
10
|
-
scrollHeight: number;
|
|
11
|
-
clientHeight: number;
|
|
12
7
|
};
|
|
13
8
|
export const TIP_GAP: number;
|
|
14
9
|
export const TIP_MARGIN: number;
|
|
15
10
|
export const TIP_ID: string;
|
|
16
11
|
export function placeTip(anchor: Box, tip: { width: number; height: number }, view: { width: number; height: number }): { left: number; top: number; side: "above" | "below" };
|
|
17
|
-
export function isTruncated(el: { scrollWidth: number; clientWidth: number; scrollHeight: number; clientHeight: number }): boolean;
|
|
18
12
|
export function adoptTitle(el: Pick<TipElement, "getAttribute" | "setAttribute" | "removeAttribute" | "hasAttribute">): void;
|
|
19
13
|
export function tipText(el: TipElement): string;
|
|
20
14
|
export function addId(list: string | null, id: string): string;
|
package/web/tooltip.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Bug 90: the page draws its own tooltip. Some browsers (the user's, likely Aside) never show the native `title`
|
|
2
|
-
// tooltip, so
|
|
3
|
-
//
|
|
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` (새 세션, the voice buttons, 확인함, 이벤트) keep a
|
|
4
|
+
// visible tip; every other `title` moves to `aria-description`, so neither the page nor the browser shows a tip for it.
|
|
4
5
|
|
|
5
6
|
export const TIP_GAP = 6;
|
|
6
7
|
export const TIP_MARGIN = 8;
|
|
7
8
|
export const TIP_ID = "zgw-tip";
|
|
8
9
|
const HOVER_DELAY_MS = 250;
|
|
9
|
-
const SELECTOR = "[data-tip]
|
|
10
|
+
const SELECTOR = "[data-tip]";
|
|
10
11
|
|
|
11
12
|
export function placeTip(anchor, tip, view) {
|
|
12
13
|
const left = Math.max(TIP_MARGIN, Math.min((anchor.left + anchor.right) / 2 - tip.width / 2, view.width - tip.width - TIP_MARGIN));
|
|
@@ -18,21 +19,17 @@ export function placeTip(anchor, tip, view) {
|
|
|
18
19
|
return { left, top, side: "below" };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export function isTruncated(el) {
|
|
22
|
-
return el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
22
|
export function adoptTitle(el) {
|
|
26
23
|
if (!el.hasAttribute("title")) return;
|
|
27
24
|
const text = el.getAttribute("title");
|
|
28
25
|
el.removeAttribute("title");
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const attr = el.hasAttribute("data-tip-keep") ? "data-tip" : "aria-description";
|
|
27
|
+
if (text) el.setAttribute(attr, text);
|
|
28
|
+
else el.removeAttribute(attr);
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
export function tipText(el) {
|
|
34
|
-
|
|
35
|
-
return el.getAttribute("data-tip") || (el.hasAttribute("data-tip-overflow") ? el.textContent : "") || "";
|
|
32
|
+
return el.hasAttribute("data-tip-keep") ? el.getAttribute("data-tip") || "" : "";
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export function addId(list, id) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TurnStatsEvent {
|
|
2
|
+
readonly conv?: string;
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly seq?: number;
|
|
5
|
+
readonly data?: {
|
|
6
|
+
readonly itemId?: unknown;
|
|
7
|
+
readonly messageSeq?: unknown;
|
|
8
|
+
readonly turnId?: unknown;
|
|
9
|
+
readonly stats?: unknown;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface StatsBody {
|
|
13
|
+
readonly ownerDocument: { createElement(tag: string): StatsLine };
|
|
14
|
+
querySelector(selector: string): unknown;
|
|
15
|
+
appendChild(line: StatsLine): unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface StatsLine {
|
|
18
|
+
className: string;
|
|
19
|
+
readonly dataset: Record<string, string | undefined>;
|
|
20
|
+
setAttribute(name: string, value: string): void;
|
|
21
|
+
}
|
|
22
|
+
export function turnStatsLabel(stats: unknown): string;
|
|
23
|
+
export function createTurnStats(): {
|
|
24
|
+
reset(conv: string): void;
|
|
25
|
+
accept(event: TurnStatsEvent, body?: StatsBody): void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Unknown fields are omitted, not rendered as zero. Model time excludes tools and wall time. */
|
|
2
|
+
export function turnStatsLabel(stats) {
|
|
3
|
+
if (!stats || typeof stats !== "object") return "";
|
|
4
|
+
const parts = [];
|
|
5
|
+
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(`모델 처리 ${(stats.modelDurationMs / 1000).toFixed(1)}초`);
|
|
7
|
+
if (Number.isFinite(stats.cacheHitPercent) && stats.cacheHitPercent >= 0 && stats.cacheHitPercent <= 100) parts.push(`캐시 적중 ${stats.cacheHitPercent.toFixed(1)}%`);
|
|
8
|
+
return parts.join(" · ");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One transcript in one gateway origin. Nothing is shared in storage or across app instances.
|
|
13
|
+
* Keep unmatched stats until their final's older page arrives; never fall back to the latest row.
|
|
14
|
+
*/
|
|
15
|
+
export function createTurnStats() {
|
|
16
|
+
let conv = null;
|
|
17
|
+
const rows = new Map();
|
|
18
|
+
const labels = new Map();
|
|
19
|
+
function draw(key) {
|
|
20
|
+
const body = rows.get(key);
|
|
21
|
+
const label = labels.get(key);
|
|
22
|
+
if (!body || !label || body.querySelector(".turn-stats")) return;
|
|
23
|
+
const line = body.ownerDocument.createElement("div");
|
|
24
|
+
line.className = "turn-stats";
|
|
25
|
+
// Generated content stays out of selection text and ordinary transcript text exports.
|
|
26
|
+
// The accessible name still exposes the same metadata to assistive technology.
|
|
27
|
+
line.dataset.label = label;
|
|
28
|
+
line.setAttribute("role", "note");
|
|
29
|
+
line.setAttribute("aria-label", label);
|
|
30
|
+
body.appendChild(line);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
reset(nextConv) {
|
|
34
|
+
conv = nextConv;
|
|
35
|
+
rows.clear();
|
|
36
|
+
labels.clear();
|
|
37
|
+
},
|
|
38
|
+
accept(event, body) {
|
|
39
|
+
if (!event || event.conv !== conv || !event.data) return;
|
|
40
|
+
const data = event.data;
|
|
41
|
+
if (typeof data.itemId !== "string" || !data.itemId) return;
|
|
42
|
+
const seq = event.type === "message.final" ? event.seq : data.messageSeq;
|
|
43
|
+
if (!Number.isInteger(seq) || seq <= 0) return;
|
|
44
|
+
const key = JSON.stringify([data.itemId, seq]);
|
|
45
|
+
if (event.type === "message.final") {
|
|
46
|
+
if (!body) return;
|
|
47
|
+
rows.set(key, body);
|
|
48
|
+
draw(key);
|
|
49
|
+
} else if (event.type === "message.stats") {
|
|
50
|
+
if (typeof data.turnId !== "string" || !data.turnId) return;
|
|
51
|
+
const label = turnStatsLabel(data.stats);
|
|
52
|
+
if (!label) return;
|
|
53
|
+
if (!labels.has(key)) labels.set(key, label);
|
|
54
|
+
draw(key);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/web/update.d.ts
CHANGED
|
@@ -42,4 +42,4 @@ export function questionGate(counters: { readonly pending: number; readonly need
|
|
|
42
42
|
export function canUpdate(status: unknown): boolean;
|
|
43
43
|
export function updateStep(status: number, body: unknown): UpdateStep;
|
|
44
44
|
export function resultLine(result: unknown): string | null;
|
|
45
|
-
export function progressLine(data: unknown): string | null;
|
|
45
|
+
export function progressLine(data: unknown, loadedVersion?: string | null): string | null;
|
package/web/update.js
CHANGED
|
@@ -89,10 +89,17 @@ export function resultLine(result) {
|
|
|
89
89
|
return `${typeof result.message === "string" ? result.message : "업데이트에 실패했습니다."}${typeof result.logPath === "string" && result.logPath !== "" ? ` (로그: ${result.logPath})` : ""}`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The line for an `update.progress` payload or a GET /api/update/state body; null shows nothing. The gateway keeps the last
|
|
94
|
+
* result in state.json for good, so `loadedVersion` (the version the page first loaded on) hides an ok result that ended
|
|
95
|
+
* at that version: the page was reloaded after the update and the "reload the page" line is stale.
|
|
96
|
+
*/
|
|
97
|
+
export function progressLine(data, loadedVersion = null) {
|
|
94
98
|
if (data === null || typeof data !== "object") return null;
|
|
95
|
-
if (data.result !== undefined && data.result !== null)
|
|
99
|
+
if (data.result !== undefined && data.result !== null) {
|
|
100
|
+
if (data.result.ok === true && loadedVersion !== null && data.result.to === loadedVersion) return null;
|
|
101
|
+
return resultLine(data.result);
|
|
102
|
+
}
|
|
96
103
|
if (typeof data.label !== "string" || data.phase === "done") return null;
|
|
97
104
|
return `업데이트 중 (${data.from} → ${data.to}): ${data.label}`;
|
|
98
105
|
}
|