zigmini 0.9.0-beta.2 → 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.
Files changed (145) hide show
  1. package/README.ko.md +35 -5
  2. package/README.md +35 -5
  3. package/assets/agent/gateway-rules.md +1 -1
  4. package/assets/android/zigmini.apk +0 -0
  5. package/assets/android/zigmini.apk.json +3 -3
  6. package/assets/build.json +1 -1
  7. package/docs/manual.ko.md +51 -11
  8. package/docs/manual.md +52 -12
  9. package/host/omo-bridge/agent-control.ts +274 -0
  10. package/host/omo-bridge/bridge.ts +1 -1
  11. package/host/omo-bridge/continuity-control.ts +1 -1
  12. package/host/omo-bridge/model-control.ts +15 -3
  13. package/host/omo-bridge/task-control.ts +26 -16
  14. package/host/omo-check/patches/omo-5.1.48-compaction-recovery.patch +912 -0
  15. package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +12 -11
  16. package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
  17. package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
  18. package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
  19. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
  20. package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
  21. package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
  22. package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
  23. package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
  24. package/package.json +3 -2
  25. package/src/agent/api.ts +215 -0
  26. package/src/agent/definition.ts +177 -0
  27. package/src/agent/fields.ts +42 -0
  28. package/src/agent/files.ts +192 -0
  29. package/src/agent/legacy-files.ts +46 -0
  30. package/src/agent/list.ts +21 -0
  31. package/src/agent-rules.ts +14 -5
  32. package/src/approvals.ts +6 -1
  33. package/src/cli/main.ts +33 -0
  34. package/src/cli/messages.ts +10 -2
  35. package/src/cli/rotate-main.ts +2 -1
  36. package/src/cli/scan.ts +414 -0
  37. package/src/cli/security-import.ts +155 -0
  38. package/src/compact.ts +22 -0
  39. package/src/decisions.ts +6 -4
  40. package/src/engine/compat.json +3 -3
  41. package/src/engine/index.ts +1 -0
  42. package/src/engine/schema-guard.ts +12 -4
  43. package/src/engine/sdk.ts +8 -0
  44. package/src/formal-verification.ts +366 -0
  45. package/src/gc.ts +37 -6
  46. package/src/hub.ts +525 -85
  47. package/src/main.ts +95 -9
  48. package/src/maintenance.ts +1 -1
  49. package/src/manual.ts +9 -2
  50. package/src/omo/client.ts +2 -0
  51. package/src/profile-prompt.ts +73 -0
  52. package/src/profile.ts +33 -10
  53. package/src/protocol.ts +11 -2
  54. package/src/question-notifications.ts +4 -1
  55. package/src/reply-notifications.ts +7 -1
  56. package/src/repo-security-setting.ts +33 -0
  57. package/src/runtime/paths.ts +16 -0
  58. package/src/security/baseline.ts +170 -0
  59. package/src/security/dex-strings.ts +242 -0
  60. package/src/security/repo-scan.ts +369 -0
  61. package/src/security/revision-probe.ts +144 -0
  62. package/src/security/terms.ts +97 -0
  63. package/src/security-allow.ts +1 -0
  64. package/src/security-baseline.ts +113 -0
  65. package/src/security-check.ts +91 -16
  66. package/src/server-text.ts +38 -0
  67. package/src/store.ts +142 -16
  68. package/src/transcript.ts +7 -1
  69. package/src/ui-language.ts +33 -0
  70. package/src/update/check.ts +7 -3
  71. package/src/update/handoff-system.ts +24 -16
  72. package/src/update/handoff.ts +23 -12
  73. package/src/update/helper.ts +72 -36
  74. package/src/update/request.ts +46 -30
  75. package/src/update/state.ts +21 -11
  76. package/src/update/strategy.ts +12 -5
  77. package/src/update/target.ts +32 -15
  78. package/src/usage.ts +43 -20
  79. package/src/version.ts +39 -16
  80. package/src/voice/agents.ts +8 -3
  81. package/src/voice/install.ts +29 -18
  82. package/src/work-watch.ts +40 -18
  83. package/web/agent-icon.js +15 -9
  84. package/web/agents.d.ts +201 -0
  85. package/web/agents.js +813 -0
  86. package/web/app-download.js +32 -19
  87. package/web/app.js +1407 -278
  88. package/web/autocopy.js +6 -1
  89. package/web/boot.js +27 -8
  90. package/web/commands.js +20 -11
  91. package/web/decisions.js +70 -49
  92. package/web/devlog.js +25 -19
  93. package/web/diagram.js +8 -3
  94. package/web/enroll.js +43 -22
  95. package/web/gc.js +32 -19
  96. package/web/i18n/agent-icon.js +24 -0
  97. package/web/i18n/agents.js +48 -0
  98. package/web/i18n/app-download.js +26 -0
  99. package/web/i18n/autocopy.js +5 -0
  100. package/web/i18n/boot.js +24 -0
  101. package/web/i18n/commands.js +34 -0
  102. package/web/i18n/decisions.js +24 -0
  103. package/web/i18n/devlog.js +60 -0
  104. package/web/i18n/diagram.js +12 -0
  105. package/web/i18n/enroll.js +22 -0
  106. package/web/i18n/gc.js +38 -0
  107. package/web/i18n/lastsent.js +10 -0
  108. package/web/i18n/main.d.ts +4 -0
  109. package/web/i18n/main.js +527 -0
  110. package/web/i18n/manual.d.ts +4 -0
  111. package/web/i18n/manual.js +14 -0
  112. package/web/i18n/memo.js +30 -0
  113. package/web/i18n/notifications.js +16 -0
  114. package/web/i18n/profile.js +13 -0
  115. package/web/i18n/reference.js +26 -0
  116. package/web/i18n/security-inbox.js +40 -0
  117. package/web/i18n/slash.js +38 -0
  118. package/web/i18n/turn-stats.js +5 -0
  119. package/web/i18n/update.js +42 -0
  120. package/web/i18n/voice.js +28 -0
  121. package/web/i18n/wake.js +16 -0
  122. package/web/i18n/workid.js +10 -0
  123. package/web/i18n/workstatus.js +20 -0
  124. package/web/i18n.d.ts +20 -0
  125. package/web/i18n.js +63 -0
  126. package/web/index.html +156 -96
  127. package/web/lastsent.js +9 -4
  128. package/web/manual.d.ts +1 -1
  129. package/web/manual.js +11 -5
  130. package/web/memo.js +18 -13
  131. package/web/notifications.js +12 -7
  132. package/web/profile.js +42 -17
  133. package/web/reference.js +20 -15
  134. package/web/security-inbox.d.ts +18 -1
  135. package/web/security-inbox.js +173 -4
  136. package/web/slash.js +23 -13
  137. package/web/style.css +132 -3
  138. package/web/tooltip.js +5 -2
  139. package/web/turn-stats.js +6 -2
  140. package/web/update.js +31 -23
  141. package/web/voice.js +18 -8
  142. package/web/wake.d.ts +4 -0
  143. package/web/wake.js +7 -3
  144. package/web/workid.js +12 -2
  145. package/web/workstatus.js +36 -32
package/src/work-watch.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { blockingCompaction, COMPACTION_FAILURE_KINDS, compactionOutcome, type OmoEvent } from "./omo/client.ts";
2
+ import { say, type Text } from "./server-text.ts";
2
3
  import type { Store, WorkCommand, WorkItem, WorkUpdate } from "./store.ts";
3
4
 
4
5
  /**
@@ -58,7 +59,7 @@ export interface Verdict {
58
59
  readonly waitingFor?: "children";
59
60
  /** Machine-readable basis, e.g. no_activity, status_request_unanswered, model_error. */
60
61
  readonly reason: string;
61
- /** One short Korean line for the card's name view. */
62
+ /** One short line for the card's name view, in the language it was judged in. */
62
63
  readonly note?: string;
63
64
  /** When the basis of this verdict began (ms). The board shows it as the card's time. */
64
65
  readonly since: number;
@@ -103,7 +104,28 @@ export interface JudgeInput {
103
104
 
104
105
  const TERMINAL = new Set(["completed", "failed", "cancelled"]);
105
106
  const UNSETTLED = new Set(["pending", "sending", "unknown"]);
106
- const ERROR_LABELS = { model_error: "모델 오류", compaction_failed: "컨텍스트 압축 실패", retry_exhausted: "재시도 실패" } as const;
107
+ const ERROR_LABELS = {
108
+ model_error: { en: "Model error", ko: "모델 오류" },
109
+ compaction_failed: { en: "Context compaction failed", ko: "컨텍스트 압축 실패" },
110
+ retry_exhausted: { en: "Retry failed", ko: "재시도 실패" },
111
+ } as const satisfies Record<string, Text>;
112
+ const MODEL_RESPONSE_ERROR: Text = { en: "Model response error", ko: "모델 응답 오류" };
113
+ const NOTE_CHILDREN: Text = { en: "Running child tasks: {count}", ko: "하위 작업 {count}개 실행 중" };
114
+ const NOTE_QUIET: Text = { en: "No activity for over {duration}", ko: "{duration} 넘게 활동이 없습니다" };
115
+ const NOTE_QUIET_IN_TOOL: Text = { en: "No activity for over {duration} while a tool ({tool}) was running", ko: "도구 실행({tool}) 중 {duration} 넘게 활동이 없습니다" };
116
+ const NOTE_CANCEL_STOPPED: Text = { en: "The run stopped after the cancel command", ko: "취소 명령 뒤 실행이 멈췄습니다" };
117
+ const NOTE_UNDELIVERED: Text = { en: "The command was not confirmed as received", ko: "명령 수신을 확인하지 못했습니다" };
118
+ const NOTE_DELIVERY_CHECKING: Text = { en: "Confirming that the command was received", ko: "명령 수신을 확인하는 중입니다" };
119
+ const NOTE_REPORT: Text = { en: "Report: {summary}", ko: "보고: {summary}" };
120
+ const NOTE_REQUEST_UNANSWERED: Text = { en: "The status report request got no response", ko: "상태 보고 요청에 응답이 없습니다" };
121
+ const NOTE_REQUESTED: Text = { en: "Stopped without a report, so a status report was requested", ko: "보고 없이 멈춰 상태 보고를 요청했습니다" };
122
+ const NOTE_STOPPED: Text = { en: "Stopped without a report", ko: "보고 없이 멈췄습니다" };
123
+ const SECONDS: Text = { en: "{count} sec", ko: "{count}초" };
124
+ const MINUTES: Text = { en: "{count} min", ko: "{count}분" };
125
+ const ALERT_SUMMARY: Text = {
126
+ en: "Work watcher verdict: failed ({note}). The stored work status was not changed, and no automatic restart or cancel was done.",
127
+ ko: "작업 감시자 판정: 고장 ({note}). 저장된 작업 상태는 바꾸지 않았고 자동 재시작·취소는 하지 않았습니다.",
128
+ };
107
129
 
108
130
  function record(value: unknown): value is Record<string, unknown> {
109
131
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -128,7 +150,7 @@ function shorten(text: string, max: number): string {
128
150
  }
129
151
 
130
152
  function duration(ms: number): string {
131
- return ms < 60_000 ? `${Math.round(ms / 1000)}초` : `${Math.round(ms / 60_000)}분`;
153
+ return ms < 60_000 ? say(SECONDS, { count: Math.round(ms / 1000) }) : say(MINUTES, { count: Math.round(ms / 60_000) });
132
154
  }
133
155
 
134
156
  function iso(ms: number): string {
@@ -159,7 +181,7 @@ export function judge(input: JudgeInput, config: WatchConfig = DEFAULT_WATCH): V
159
181
  const waitSince = Date.parse(reports.at(-1)?.receivedAt ?? work.createdAt);
160
182
  const childWait = (next: number): Verdict => ({
161
183
  state: "waiting", waitingFor: "children", reason: "children_running",
162
- note: `하위 작업 ${children}개 실행 중`, since: waitSince, next,
184
+ note: say(NOTE_CHILDREN, { count: children }), since: waitSince, next,
163
185
  });
164
186
  // Without confirmed runtime evidence (no event, no reconciliation yet) the watcher shows the record and never acts.
165
187
  if (live.streaming === null) return { state: storeState(work), reason: "runtime_unknown", since: base, next: Infinity };
@@ -176,12 +198,12 @@ export function judge(input: JudgeInput, config: WatchConfig = DEFAULT_WATCH): V
176
198
  const unresponsiveAt = quietFrom + allowed;
177
199
  const brokenAt = unresponsiveAt + config.brokenAfterMs;
178
200
  const evidence = { signal: "no_activity", last_activity_at: iso(quietFrom), open_tools: tools.map((tool) => tool.name), observed_since: iso(live.observedSince) };
179
- const what = tools.length > 0 ? `도구 실행(${shorten(tools[0]?.name ?? "", 24)}) 중 ` : "";
201
+ const quiet = (ms: number): string => tools.length > 0 ? say(NOTE_QUIET_IN_TOOL, { duration: duration(ms), tool: shorten(tools[0]?.name ?? "", 24) }) : say(NOTE_QUIET, { duration: duration(ms) });
180
202
  if (now >= brokenAt) {
181
- return { state: "broken", reason: "no_activity", note: `${what}${duration(allowed + config.brokenAfterMs)} 넘게 활동이 없습니다`, since: quietFrom, next: Infinity, evidence };
203
+ return { state: "broken", reason: "no_activity", note: quiet(allowed + config.brokenAfterMs), since: quietFrom, next: Infinity, evidence };
182
204
  }
183
205
  if (now >= unresponsiveAt) {
184
- return { state: "unresponsive", reason: "no_activity", note: `${what}${duration(allowed)} 넘게 활동이 없습니다`, since: quietFrom, next: brokenAt, evidence };
206
+ return { state: "unresponsive", reason: "no_activity", note: quiet(allowed), since: quietFrom, next: brokenAt, evidence };
185
207
  }
186
208
  return { state: "running", reason: "active", since: base, next: unresponsiveAt };
187
209
  }
@@ -191,13 +213,13 @@ export function judge(input: JudgeInput, config: WatchConfig = DEFAULT_WATCH): V
191
213
  if (children > 0) return childWait(Infinity);
192
214
  if (command.kind === "cancel" && command.delivery === "accepted") {
193
215
  const at = Math.max(stoppedAt, Date.parse(command.acceptedAt ?? command.createdAt));
194
- if (now >= at + config.settleMs) return { state: "cancelled", reason: "cancel_stopped", note: "취소 명령 뒤 실행이 멈췄습니다", since: at, next: Infinity };
216
+ if (now >= at + config.settleMs) return { state: "cancelled", reason: "cancel_stopped", note: say(NOTE_CANCEL_STOPPED), since: at, next: Infinity };
195
217
  return { state: "unknown", reason: "cancel_settling", since: at, next: at + config.settleMs };
196
218
  }
197
219
  const error = live.lastError;
198
220
  if (error && error.at >= commandAt && !(latestReport && Date.parse(latestReport.receivedAt) > error.at)) {
199
221
  return {
200
- state: "broken", reason: error.kind, note: `${ERROR_LABELS[error.kind]}: ${shorten(error.message, 80)}`, since: error.at, next: Infinity,
222
+ state: "broken", reason: error.kind, note: `${say(ERROR_LABELS[error.kind])}: ${shorten(error.message, 80)}`, since: error.at, next: Infinity,
201
223
  evidence: { signal: error.kind, at: iso(error.at), message: shorten(error.message, 300) },
202
224
  };
203
225
  }
@@ -206,23 +228,23 @@ export function judge(input: JudgeInput, config: WatchConfig = DEFAULT_WATCH): V
206
228
  const at = Math.max(stoppedAt, commandAt);
207
229
  const brokenAt = at + config.reportResponseMs;
208
230
  const evidence = { signal: "command_delivery_unknown", command_seq: command.seq, error: command.error, since: iso(at) };
209
- if (now >= brokenAt) return { state: "broken", reason: "command_undelivered", note: "명령 수신을 확인하지 못했습니다", since: at, next: Infinity, evidence };
210
- return { state: "unknown", reason: "command_delivery_unknown", note: "명령 수신을 확인하는 중입니다", since: at, next: brokenAt };
231
+ if (now >= brokenAt) return { state: "broken", reason: "command_undelivered", note: say(NOTE_UNDELIVERED), since: at, next: Infinity, evidence };
232
+ return { state: "unknown", reason: "command_delivery_unknown", note: say(NOTE_DELIVERY_CHECKING), since: at, next: brokenAt };
211
233
  }
212
234
  if (command.delivery !== "accepted") return { state: "unknown", reason: `command_${command.delivery}`, since: base, next: Infinity };
213
235
  if (latestReport) {
214
- return { state: "waiting", reason: `reported_${latestReport.kind}`, note: `보고: ${shorten(latestReport.summary, 80)}`, since: Date.parse(latestReport.receivedAt), next: Infinity };
236
+ return { state: "waiting", reason: `reported_${latestReport.kind}`, note: say(NOTE_REPORT, { summary: shorten(latestReport.summary, 80) }), since: Date.parse(latestReport.receivedAt), next: Infinity };
215
237
  }
216
238
  if (isStatusRequest(command)) {
217
239
  const at = Math.max(stoppedAt, Date.parse(command.acceptedAt ?? command.createdAt));
218
240
  const brokenAt = at + config.reportResponseMs;
219
241
  if (now >= brokenAt) {
220
242
  return {
221
- state: "broken", reason: "status_request_unanswered", note: "상태 보고 요청에 응답이 없습니다", since: at, next: Infinity,
243
+ state: "broken", reason: "status_request_unanswered", note: say(NOTE_REQUEST_UNANSWERED), since: at, next: Infinity,
222
244
  evidence: { signal: "status_request_unanswered", command_seq: command.seq, requested_at: command.createdAt, stopped_since: iso(at) },
223
245
  };
224
246
  }
225
- return { state: "unknown", reason: "status_requested", note: "보고 없이 멈춰 상태 보고를 요청했습니다", since: at, next: brokenAt };
247
+ return { state: "unknown", reason: "status_requested", note: say(NOTE_REQUESTED), since: at, next: brokenAt };
226
248
  }
227
249
  // The latest command gets the full window from its own acceptance, however long the work had been stopped before it.
228
250
  const requestFrom = Math.max(stoppedAt, Date.parse(command.acceptedAt ?? command.createdAt));
@@ -230,7 +252,7 @@ export function judge(input: JudgeInput, config: WatchConfig = DEFAULT_WATCH): V
230
252
  const interrupted = work.statusReason === "omo_disconnected";
231
253
  const requestAt = interrupted ? requestFrom : requestFrom + config.reportRequestMs;
232
254
  if (now >= requestAt && !commands.some((candidate) => UNSETTLED.has(candidate.delivery))) {
233
- return { state: "unknown", reason: "stopped_without_report", note: "보고 없이 멈췄습니다", since: requestFrom, next: Infinity, action: "request_status" };
255
+ return { state: "unknown", reason: "stopped_without_report", note: say(NOTE_STOPPED), since: requestFrom, next: Infinity, action: "request_status" };
234
256
  }
235
257
  return { state: "unknown", reason: "stopped", since: requestFrom, next: requestAt };
236
258
  }
@@ -368,7 +390,7 @@ export class WorkWatcher {
368
390
  // even with errorMessage set; a work that really stops afterwards is judged by the status request (bug 23).
369
391
  if (message.stopReason === "aborted") return false;
370
392
  if (message.stopReason === "error" || typeof message.errorMessage === "string") {
371
- live.lastError = { kind: "model_error", message: typeof message.errorMessage === "string" ? message.errorMessage : "모델 응답 오류", at: now };
393
+ live.lastError = { kind: "model_error", message: typeof message.errorMessage === "string" ? message.errorMessage : say(MODEL_RESPONSE_ERROR), at: now };
372
394
  } else {
373
395
  live.lastError = null;
374
396
  }
@@ -388,7 +410,7 @@ export class WorkWatcher {
388
410
  case "auto_retry_end":
389
411
  // A user abort cancels a pending retry; that is not a breakdown.
390
412
  if (event.success !== false || event.finalError === "Retry cancelled") return false;
391
- live.lastError = { kind: "retry_exhausted", message: typeof event.finalError === "string" ? event.finalError : "재시도 실패", at: now };
413
+ live.lastError = { kind: "retry_exhausted", message: typeof event.finalError === "string" ? event.finalError : say(ERROR_LABELS.retry_exhausted), at: now };
392
414
  return true;
393
415
  case "extension_event": {
394
416
  if (event.name !== "omo.task.updated" || !record(event.data) || !Array.isArray(event.data.tasks)) return false;
@@ -510,7 +532,7 @@ export class WorkWatcher {
510
532
  const recorded = this.store.recordWatchAlert(work.id, {
511
533
  sourceKey: `watch:broken:${work.id}:${command.seq}`,
512
534
  commandSeq: command.seq,
513
- summary: `작업 감시자 판정: 고장 (${verdict.note ?? verdict.reason}). 저장된 작업 상태는 바꾸지 않았고 자동 재시작·취소는 하지 않았습니다.`,
535
+ summary: say(ALERT_SUMMARY, { note: verdict.note ?? verdict.reason }),
514
536
  details: { source: "watcher", verdict: "broken", reason: verdict.reason, judged_at: iso(this.clock()), evidence: verdict.evidence ?? null },
515
537
  });
516
538
  if (recorded) this.hooks.alerted(work);
package/web/agent-icon.js CHANGED
@@ -1,3 +1,8 @@
1
+ import dict from "./i18n/agent-icon.js";
2
+ import { applyI18n, registerStrings, t } from "./i18n.js";
3
+
4
+ registerStrings(dict);
5
+
1
6
  // Improvement 60: the agent's (Zig's) profile icon. Double-clicking the agent avatar opens a small editor: choose a
2
7
  // PNG/JPEG/WebP of at most 1 MB, see it center-cropped to a square, save it, or go back to the default icon. Escape closes
3
8
  // the editor without a change. The gateway keeps the image (/api/profile/icon); `agentIcon` in the profile is its version.
@@ -16,8 +21,8 @@ export function agentIconUrl(version) {
16
21
 
17
22
  /** Null when [file] can be used, else the Korean message to show. */
18
23
  export function iconFileProblem(file) {
19
- if (!ICON_TYPES.includes(file?.type)) return "PNG, JPEG, WebP 이미지만 쓸 수 있습니다.";
20
- if (file.size > ICON_MAX_BYTES) return "이미지가 1MB보다 큽니다. 더 작은 이미지를 고르세요.";
24
+ if (!ICON_TYPES.includes(file?.type)) return t("icon.problem.type");
25
+ if (file.size > ICON_MAX_BYTES) return t("icon.problem.size");
21
26
  return null;
22
27
  }
23
28
 
@@ -58,17 +63,18 @@ export function editAgentIcon(host, currentUrl, { onSave, onReset }) {
58
63
  const editor = document.createElement("div");
59
64
  editor.className = "icon-editor";
60
65
  editor.setAttribute("role", "dialog");
61
- editor.setAttribute("aria-label", "에이전트 아이콘 바꾸기");
66
+ editor.setAttribute("aria-label", t("icon.editor.label"));
62
67
  editor.innerHTML = `<img class="icon-editor-preview" alt="" width="64" height="64" />
63
- <div class="icon-editor-hint">PNG, JPEG, WebP · 1MB 이하 · 가운데를 정사각형으로 자릅니다</div>
68
+ <div class="icon-editor-hint" data-i18n="icon.editor.hint"></div>
64
69
  <div class="icon-editor-error" role="alert" hidden></div>
65
70
  <input type="file" accept="${ICON_TYPES.join(",")}" hidden />
66
71
  <div class="icon-editor-actions">
67
- <button type="button" data-act="pick">이미지 선택</button>
68
- <button type="button" data-act="save" disabled>저장</button>
69
- <button type="button" data-act="reset">기본 아이콘</button>
70
- <button type="button" data-act="cancel">취소</button>
72
+ <button type="button" data-act="pick" data-i18n="icon.pick"></button>
73
+ <button type="button" data-act="save" data-i18n="icon.save" disabled></button>
74
+ <button type="button" data-act="reset" data-i18n="icon.reset"></button>
75
+ <button type="button" data-act="cancel" data-i18n="icon.cancel"></button>
71
76
  </div>`;
77
+ applyI18n(editor);
72
78
  const preview = editor.querySelector(".icon-editor-preview");
73
79
  const error = editor.querySelector(".icon-editor-error");
74
80
  const input = editor.querySelector("input");
@@ -107,7 +113,7 @@ export function editAgentIcon(host, currentUrl, { onSave, onReset }) {
107
113
  try {
108
114
  chosen = await squarePng(file);
109
115
  } catch {
110
- showError("이미지를 읽지 못했습니다. 다른 이미지를 고르세요.");
116
+ showError(t("icon.readFailed"));
111
117
  return;
112
118
  }
113
119
  showError(null);
@@ -0,0 +1,201 @@
1
+ export type FieldOrigin = "package" | "defaults" | "agent" | "none";
2
+ export type FieldWidget = "text" | "textarea" | "number" | "toggle" | "model" | "select" | "skills" | "group";
3
+ export type FieldType = "string" | "integer" | "boolean" | "object" | "strings";
4
+ export type EditorKind = "new" | "agent" | "defaults";
5
+
6
+ /** One gateway registry row as the agent API sends it (`registry` in detail and defaults answers; an absent default is undefined). */
7
+ export interface AgentFieldMeta {
8
+ readonly name: string;
9
+ readonly type: FieldType;
10
+ readonly default?: unknown;
11
+ readonly required: boolean;
12
+ readonly merge: "replace" | "keys";
13
+ readonly applies: "gateway" | "extension";
14
+ readonly widget: FieldWidget;
15
+ readonly forbidden: boolean;
16
+ }
17
+
18
+ /** The editor's labels and hints for one registry field name. */
19
+ export interface FieldPresentation {
20
+ readonly label: string;
21
+ readonly help?: string;
22
+ readonly options?: readonly string[];
23
+ readonly min?: number;
24
+ readonly max?: number;
25
+ }
26
+
27
+ /** A registry row joined with its presentation: what editorRows() and widgetValue() work from. */
28
+ export interface AgentFieldRow extends AgentFieldMeta {
29
+ readonly label: string;
30
+ readonly help: string;
31
+ readonly options?: readonly string[];
32
+ readonly min?: number;
33
+ readonly max?: number;
34
+ }
35
+
36
+ /** GET /api/agents rows and `agent.updated` payloads. */
37
+ export interface AgentSummary {
38
+ readonly id: string;
39
+ readonly name: string;
40
+ readonly description: string;
41
+ readonly icon: string | null;
42
+ readonly convId: string;
43
+ readonly definitionSha: string;
44
+ readonly stale: boolean;
45
+ }
46
+
47
+ /** GET /api/agents/:id: the summary with the definition text, the merged result and the field registry. */
48
+ export interface AgentDetail extends AgentSummary {
49
+ readonly source: string;
50
+ readonly fields: Readonly<Record<string, unknown>>;
51
+ readonly body: string;
52
+ readonly defaultsBody: string;
53
+ readonly origins: Readonly<Record<string, "package" | "defaults" | "agent">>;
54
+ readonly sha: string;
55
+ readonly registry: readonly AgentFieldMeta[];
56
+ }
57
+
58
+ /** GET and PUT /api/agents/defaults: the defaults.md text and the same field registry. */
59
+ export interface AgentDefaults {
60
+ readonly source: string;
61
+ readonly registry: readonly AgentFieldMeta[];
62
+ }
63
+
64
+ export interface ConversationRow {
65
+ readonly id: string;
66
+ readonly title: string;
67
+ readonly threadId?: string | null;
68
+ readonly createdAt?: string;
69
+ readonly agentId: string | null;
70
+ }
71
+
72
+ export interface SourceSplit {
73
+ readonly ok: boolean;
74
+ readonly preamble: string;
75
+ readonly entries: readonly { readonly key: string | null; readonly raw: string }[];
76
+ readonly body: string;
77
+ }
78
+
79
+ export interface EditorState {
80
+ readonly kind: EditorKind;
81
+ readonly registry: readonly AgentFieldRow[];
82
+ readonly id: string | null;
83
+ readonly name: string | null;
84
+ readonly convId: string | null;
85
+ readonly stale: boolean;
86
+ readonly own: Record<string, unknown>;
87
+ readonly rawByKey: Readonly<Record<string, string>>;
88
+ readonly touched: ReadonlySet<string>;
89
+ readonly unparsed: ReadonlySet<string>;
90
+ readonly unknownRaw: string;
91
+ readonly unknownTouched: boolean;
92
+ readonly preamble: string;
93
+ readonly body: string;
94
+ readonly bodyTouched: boolean;
95
+ readonly inherited: Readonly<Record<string, unknown>>;
96
+ readonly origins: Readonly<Record<string, string>>;
97
+ readonly dirty: boolean;
98
+ }
99
+
100
+ export type EditorRow =
101
+ | { readonly kind: "group"; readonly name: string; readonly label: string; readonly help: string }
102
+ | {
103
+ readonly kind: "field";
104
+ readonly name: string;
105
+ readonly label: string;
106
+ readonly help: string;
107
+ readonly widget: FieldWidget;
108
+ readonly type: FieldType;
109
+ readonly required: boolean;
110
+ readonly value: unknown;
111
+ readonly own: boolean;
112
+ readonly origin: FieldOrigin;
113
+ readonly originLabel: string;
114
+ readonly clearable: boolean;
115
+ readonly unparsed: boolean;
116
+ readonly options?: readonly string[];
117
+ readonly min?: number;
118
+ readonly max?: number;
119
+ };
120
+
121
+ /** What widgetValue() reads from a registry row or an editor field row. */
122
+ export type WidgetSpec = Pick<AgentFieldRow, "widget" | "required" | "label"> & Partial<Pick<AgentFieldRow, "options" | "min" | "max">>;
123
+ export interface EditorError { readonly name: string; readonly message: string }
124
+ export interface ApiRequest { readonly method: "GET" | "POST" | "PUT"; readonly path: string; readonly headers?: Readonly<Record<string, string>>; readonly body?: string }
125
+
126
+ export interface SidebarItem {
127
+ readonly convId: string;
128
+ readonly agentId: string;
129
+ readonly name: string;
130
+ readonly description: string;
131
+ readonly icon: string;
132
+ readonly active: boolean;
133
+ readonly running: boolean;
134
+ readonly pending: boolean;
135
+ readonly stale: boolean;
136
+ }
137
+
138
+ export interface SidebarView {
139
+ readonly current: string;
140
+ readonly running: ReadonlySet<string>;
141
+ readonly busy: (convId: string) => boolean;
142
+ readonly pending: ReadonlyMap<string, number>;
143
+ }
144
+
145
+ export interface HeaderView { readonly agent: boolean; readonly agentId: string | null; readonly icon: string; readonly name: string; readonly stale: boolean }
146
+ export interface TrashRow { readonly entry: string; readonly id: string | null; readonly name: string; readonly description: string; readonly icon: string; readonly deletedAt: string | null }
147
+ export interface FileRow { readonly path: string; readonly type: "file" | "directory"; readonly size: number; readonly depth: number; readonly name: string; readonly label: string; readonly source: boolean }
148
+ export interface AgentUpdateResult<P, C> { readonly agents: readonly P[]; readonly conversations: readonly C[]; readonly removedConv: string | null; readonly restoredConv: string | null }
149
+
150
+ export const AGENTS_PATH: string;
151
+ export const DEFAULTS_PATH: string;
152
+ export const TRASH_PATH: string;
153
+ export const AGENT_MAX_BYTES: number;
154
+ export const DESCRIPTION_MAX: number;
155
+ export const MEMORY_MAX_BYTES: number;
156
+ export const THINKING_LEVELS: readonly string[];
157
+ export const ORIGIN_LABELS: Readonly<Record<FieldOrigin, string>>;
158
+ export const DEFAULT_ICON: string;
159
+ export const STALE_LABEL: string;
160
+ export const STALE_TITLE: string;
161
+ export const DELETE_HINT: string;
162
+ export const EMPTY_AGENTS_LINE: string;
163
+ export const EMPTY_TRASH_LINE: string;
164
+ export const EMPTY_FILES_LINE: string;
165
+ export const NEW_AGENT_BODY: string;
166
+ export const FIELD_PRESENTATION: Readonly<Record<string, FieldPresentation>>;
167
+
168
+ export function fieldAt(fields: unknown, name: string): unknown;
169
+ export function editorFields(registry: readonly AgentFieldMeta[] | null | undefined): AgentFieldRow[];
170
+ export function topLevelKeys(registry: readonly AgentFieldMeta[]): string[];
171
+ export function parseYaml(text: string): unknown;
172
+ export function emitScalar(value: unknown): string;
173
+ export function emitEntry(key: string, value: unknown): string;
174
+ export function splitSource(source: string): SourceSplit;
175
+ export function editorState(detail: Partial<AgentDetail> | Partial<AgentDefaults> | null | undefined, kind?: EditorKind): EditorState;
176
+ export function packageDefaults(registry: readonly AgentFieldMeta[]): Record<string, unknown>;
177
+ export function editorRows(state: EditorState): EditorRow[];
178
+ export function widgetValue(field: WidgetSpec, input: unknown): { value?: unknown; error?: string };
179
+ export function setField(state: EditorState, name: string, value: unknown): EditorState;
180
+ export function clearField(state: EditorState, name: string): EditorState;
181
+ export function setBody(state: EditorState, body: string): EditorState;
182
+ export function setUnknownRaw(state: EditorState, unknownRaw: string): EditorState;
183
+ export function composeSource(state: EditorState): string;
184
+ export function validateEditor(state: EditorState): EditorError[];
185
+ export function saveRequest(state: EditorState): ApiRequest;
186
+ export function deleteEnabled(typed: string, name: string | null | undefined): boolean;
187
+ export function deleteRequest(id: string, name: string): ApiRequest;
188
+ export function cloneRequest(id: string): ApiRequest;
189
+ export function restoreRequest(entry: string): ApiRequest;
190
+ export function filesPath(id: string): string;
191
+ export function sourceUploadPath(id: string): string;
192
+ export function errorLine(body: unknown, status: number): string;
193
+ export function applyAgentUpdate<P extends { id: string; convId?: string }, C extends ConversationRow>(agents: readonly P[], conversations: readonly C[], data: unknown, now?: number): AgentUpdateResult<P | AgentSummary, C | ConversationRow>;
194
+ export function applyAgentApplied<P extends { convId: string; definitionSha: string; stale: boolean }>(agents: readonly P[], convId: string, data: unknown): P[];
195
+ export function mainConversations<C extends ConversationRow>(conversations: readonly C[]): C[];
196
+ export function shownConversation(conversations: readonly ConversationRow[], id: string): string;
197
+ export function agentInitial(name: unknown): string;
198
+ export function sidebarItems(conversations: readonly ConversationRow[], agents: readonly Partial<AgentSummary>[], view: SidebarView): SidebarItem[];
199
+ export function headerView(conv: ConversationRow | null | undefined, agents: readonly Partial<AgentSummary>[]): HeaderView;
200
+ export function trashRows(entries: unknown): TrashRow[];
201
+ export function fileRows(entries: unknown): FileRow[];