zigmini 1.0.0-beta.3 → 1.1.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 (138) hide show
  1. package/README.ko.md +27 -2
  2. package/README.md +27 -2
  3. package/assets/agent/gateway-request-rules.md +1 -1
  4. package/assets/agent/gateway-rules.md +6 -1
  5. package/assets/android/zigmini.apk +0 -0
  6. package/assets/android/zigmini.apk.json +3 -3
  7. package/assets/build.json +1 -1
  8. package/assets/document-reader/document-reader +0 -0
  9. package/assets/document-reader/host-selector +0 -0
  10. package/assets/document-reader/manifest.json +27 -0
  11. package/docs/manual.ko.md +114 -10
  12. package/docs/manual.md +117 -11
  13. package/host/document-reader/DocumentRead.swift +370 -0
  14. package/host/document-reader/HostSelector.swift +192 -0
  15. package/host/document-reader/Protocol.swift +647 -0
  16. package/host/document-reader/main.swift +22 -0
  17. package/host/omo-bridge/agent-aside-tool.ts +119 -0
  18. package/host/omo-bridge/agent-aside.ts +522 -0
  19. package/host/omo-bridge/agent-collaboration-files.ts +282 -0
  20. package/host/omo-bridge/agent-collaboration.ts +385 -0
  21. package/host/omo-bridge/agent-control.ts +400 -28
  22. package/host/omo-bridge/agent-execution.sb +42 -0
  23. package/host/omo-bridge/agent-execution.ts +158 -0
  24. package/host/omo-bridge/agent-tool-adapters.ts +388 -0
  25. package/host/omo-bridge/agent-tool-permissions.ts +96 -0
  26. package/host/omo-bridge/aside-tabs.ts +41 -0
  27. package/host/omo-bridge/attachment-read.ts +100 -0
  28. package/host/omo-bridge/bridge.ts +10 -0
  29. package/host/omo-bridge/managed-call.ts +46 -0
  30. package/host/omo-bridge/task-control.ts +39 -5
  31. package/host/omo-check/patches/attachments-293/README.md +138 -0
  32. package/host/omo-check/patches/attachments-293/document-content.js +234 -0
  33. package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
  34. package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
  35. package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
  36. package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
  37. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +28 -6
  38. package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
  39. package/package.json +5 -3
  40. package/src/agent/api.ts +345 -20
  41. package/src/agent/artifacts.ts +196 -0
  42. package/src/agent/aside-target-routes.ts +369 -0
  43. package/src/agent/aside-targets.ts +215 -0
  44. package/src/agent/collaboration-admission.ts +47 -0
  45. package/src/agent/collaboration-read.ts +117 -0
  46. package/src/agent/collaboration-session.ts +196 -0
  47. package/src/agent/collaboration-types.ts +258 -0
  48. package/src/agent/collaboration.ts +399 -0
  49. package/src/agent/definition.ts +65 -3
  50. package/src/agent/fields.ts +9 -3
  51. package/src/agent/file-core.ts +325 -0
  52. package/src/agent/files.ts +87 -8
  53. package/src/agent/list.ts +3 -3
  54. package/src/agent/managed-call.ts +144 -0
  55. package/src/agent/tool-catalog.ts +184 -0
  56. package/src/agent/tool-delivery.ts +359 -0
  57. package/src/agent/tool-permission-types.ts +81 -0
  58. package/src/agent/tool-permissions.ts +1366 -0
  59. package/src/attachments/api.ts +161 -0
  60. package/src/attachments/document-reader.ts +325 -0
  61. package/src/attachments/host-selector.ts +410 -0
  62. package/src/attachments/managed-binding.ts +83 -0
  63. package/src/attachments/prepare.ts +214 -0
  64. package/src/attachments/selector-port.ts +28 -0
  65. package/src/attachments/service.ts +729 -0
  66. package/src/attachments/store.ts +698 -0
  67. package/src/attachments/types.ts +123 -0
  68. package/src/cli/doctor.ts +30 -10
  69. package/src/decisions.ts +109 -10
  70. package/src/engine/apply-patch.ts +21 -0
  71. package/src/engine/compat.json +2 -1
  72. package/src/engine/install.ts +45 -4
  73. package/src/engine/sandbox-cell.ts +2 -0
  74. package/src/gc.ts +33 -225
  75. package/src/housekeeping/api.ts +163 -0
  76. package/src/housekeeping/audit.ts +61 -0
  77. package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
  78. package/src/housekeeping/collectors/agent-drafts.ts +43 -0
  79. package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
  80. package/src/housekeeping/collectors/agent-trash.ts +28 -0
  81. package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
  82. package/src/housekeeping/collectors/db-backup.ts +25 -0
  83. package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
  84. package/src/housekeeping/collectors/docker-image.ts +135 -0
  85. package/src/housekeeping/collectors/omo-version.ts +32 -0
  86. package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
  87. package/src/housekeeping/collectors/reconcile-work.ts +55 -0
  88. package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
  89. package/src/housekeeping/collectors/shared.ts +149 -0
  90. package/src/housekeeping/collectors/temp-dir.ts +28 -0
  91. package/src/housekeeping/collectors/update-file.ts +25 -0
  92. package/src/housekeeping/collectors/worktree.ts +193 -0
  93. package/src/housekeeping/engine.ts +125 -0
  94. package/src/housekeeping/reconcile/work.ts +130 -0
  95. package/src/housekeeping/registry.ts +63 -0
  96. package/src/housekeeping/trash.ts +59 -0
  97. package/src/housekeeping/types.ts +31 -0
  98. package/src/hub.ts +1095 -41
  99. package/src/main.ts +228 -30
  100. package/src/message-references.ts +14 -5
  101. package/src/profile-prompt.ts +13 -1
  102. package/src/protocol.ts +41 -6
  103. package/src/security-allow.ts +79 -2
  104. package/src/security-check.ts +1 -1
  105. package/src/store.ts +1946 -23
  106. package/src/test-crash.ts +20 -0
  107. package/src/work-watch.ts +55 -7
  108. package/src/work.ts +192 -3
  109. package/web/DESIGN.md +33 -0
  110. package/web/agents.d.ts +189 -12
  111. package/web/agents.js +898 -39
  112. package/web/app.js +2634 -210
  113. package/web/attachments.d.ts +91 -0
  114. package/web/attachments.js +422 -0
  115. package/web/collaboration.d.ts +87 -0
  116. package/web/collaboration.js +290 -0
  117. package/web/decisions.d.ts +39 -2
  118. package/web/decisions.js +354 -2
  119. package/web/gc.d.ts +26 -5
  120. package/web/gc.js +113 -13
  121. package/web/i18n/agents.d.ts +4 -0
  122. package/web/i18n/agents.js +200 -8
  123. package/web/i18n/attachments.js +78 -0
  124. package/web/i18n/decisions.d.ts +4 -0
  125. package/web/i18n/decisions.js +68 -0
  126. package/web/i18n/gc.d.ts +4 -0
  127. package/web/i18n/gc.js +184 -2
  128. package/web/i18n/main.js +80 -8
  129. package/web/i18n/tour.d.ts +4 -0
  130. package/web/i18n/tour.js +37 -0
  131. package/web/index.html +20 -3
  132. package/web/profile.d.ts +2 -2
  133. package/web/profile.js +11 -6
  134. package/web/style.css +342 -21
  135. package/web/tooltip.d.ts +2 -0
  136. package/web/tooltip.js +12 -0
  137. package/web/tour.d.ts +57 -0
  138. package/web/tour.js +450 -0
package/web/style.css CHANGED
@@ -8,13 +8,33 @@
8
8
  --muted: #949ba4;
9
9
  --accent: #5865f2;
10
10
  --ok: #23a55a;
11
+ --ok-soft: rgba(35, 165, 90, 0.18);
11
12
  --warn: #f0b232;
13
+ --warn-soft: rgba(240, 178, 50, 0.16);
12
14
  --danger: #da373c;
13
15
  --border: #1f2023;
14
16
  --side-w: 512px;
15
17
  font-family: Pretendard, "Noto Sans KR", system-ui, sans-serif;
16
18
  }
17
19
  * { box-sizing: border-box; }
20
+ /* The document tray under the composer: wrapping chips in the chat's tokens (the @ menu itself is styled with the slash menu below). */
21
+ .document-list:empty, .host-file-status:empty { display: none; }
22
+ .document-list { flex: none; display: flex; flex-wrap: wrap; gap: 8px; padding: 0; margin: 0 16px 8px; list-style: none; max-width: 100%; }
23
+ .document-chip { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; max-width: 100%; padding: 8px; background: var(--bg-card); color: var(--text); border: 1px solid #4e5058; border-radius: 6px; font-size: 13px; overflow-wrap: anywhere; }
24
+ .document-chip strong, .document-chip a { min-width: 0; overflow-wrap: anywhere; color: var(--text); }
25
+ .document-chip a { display: inline-flex; align-items: center; min-width: 24px; min-height: 24px; }
26
+ .document-chip span { flex: 1 0 100%; font-size: 12px; color: var(--muted); }
27
+ .document-chip select { max-width: 100%; min-height: 24px; font: inherit; font-size: 12px; background: var(--bg-input); color: var(--text); border: 1px solid #4e5058; border-radius: 4px; }
28
+ .document-chip .text-btn { min-height: 24px; }
29
+ .document-chip button:hover:not(:disabled) { background: var(--bg-hover); }
30
+ .document-chip button:active:not(:disabled) { background: var(--bg-input); }
31
+ .document-chip :disabled { color: var(--muted); opacity: 1; cursor: default; }
32
+ .document-chip.revoked strong { color: var(--muted); }
33
+ .document-chip :focus-visible, #input:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
34
+ #document-tray .document-chip { gap: 6px; padding: 2px 2px 2px 8px; background: transparent; border-radius: 4px; }
35
+ #document-tray .document-chip strong { font-weight: 400; }
36
+ #document-tray .document-chip span { flex: 0 1 auto; }
37
+ @media (max-width: 640px) { #document-tray .document-chip span { flex: 1 1 80px; min-width: 0; } }
18
38
  html, body { margin: 0; height: 100%; background: var(--bg-chat); color: var(--text); }
19
39
  body { word-break: keep-all; overflow-wrap: anywhere; overflow: hidden; }
20
40
  button { font: inherit; color: inherit; cursor: pointer; }
@@ -60,6 +80,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
60
80
  .me-name { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
61
81
  .agent-avatar { background: var(--bg-input); overflow: hidden; }
62
82
  .agent-avatar img { width: 100%; height: 100%; display: block; object-fit: cover; }
83
+ .bot-avatar, .agent-avatar { font-size: 18px; line-height: 1; }
63
84
  .me { user-select: none; }
64
85
  .me-name-input { width: 100%; min-width: 0; box-sizing: border-box; font: inherit; color: var(--text); background: var(--bg-input); border: 1px solid var(--accent); border-radius: 4px; padding: 1px 6px; outline: none; }
65
86
  .me-name-input[aria-invalid="true"] { border-color: var(--danger); }
@@ -168,6 +189,10 @@ button { font: inherit; color: inherit; cursor: pointer; }
168
189
  .msg-text th { background: #2b2d31; }
169
190
  .msg-text hr { border: 0; border-top: 1px solid #4e5058; }
170
191
  .text-btn.armed { background: var(--warn); color: #111; }
192
+ /* Bug 283: the new-session button carries the action while the open agent's definition changed: warning border and fill, ↻ before the label. */
193
+ .text-btn.attention { box-shadow: inset 0 0 0 1px var(--warn); background: var(--warn-soft); color: var(--warn); font-weight: 600; }
194
+ .text-btn.attention::before { content: "\21BB"; margin-right: 4px; }
195
+ .text-btn.attention:hover { background: var(--warn); color: #111; }
171
196
  .msg-text.streaming::after { content: "▍"; color: var(--muted); }
172
197
  .msg-text:empty:not(.streaming) { display: none; }
173
198
  .thinking { margin: 2px 0 6px; border-left: 3px solid #5c5f66; padding: 2px 0 2px 10px; color: var(--muted); font-size: 13px; }
@@ -188,6 +213,44 @@ button { font: inherit; color: inherit; cursor: pointer; }
188
213
  .approval-options .btn { text-align: left; }
189
214
  .approval-options .btn small { display: block; font-weight: 400; font-size: 12px; opacity: 0.85; }
190
215
  .approval-options .btn.picked { background: var(--ok); }
216
+ /* 에이전트 협업 카드 (292 C-10): one card per request, aligned with message text (68px), the state as a dot plus a word, never colour alone. */
217
+ .collaboration-card { margin: 8px 16px 8px 68px; max-width: 640px; border: 1px solid #4e5058; border-radius: 6px; background: var(--bg-card); padding: 12px; font-size: 14px; }
218
+ .collab-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 8px; }
219
+ .collab-title { font-weight: 700; }
220
+ .collab-kind { font-size: 11px; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; white-space: nowrap; }
221
+ .collab-state { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); white-space: nowrap; }
222
+ .collab-state::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }
223
+ .collab-state.ok { color: var(--ok); }
224
+ .collab-state.busy { color: #a3acff; }
225
+ .collab-state.warn { color: var(--warn); }
226
+ .collab-state.danger { color: #f38b8d; }
227
+ .collab-time { margin-left: auto; font-size: 12px; color: var(--muted); }
228
+ .collab-parties { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 4px; font-size: 12px; color: var(--muted); }
229
+ .collab-parties b { color: var(--text); font-weight: 600; }
230
+ .collab-text { margin-top: 8px; white-space: pre-wrap; line-height: 1.5; }
231
+ .collab-section { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 8px; }
232
+ .collab-section-label { font-size: 12px; font-weight: 600; color: var(--muted); margin: 0 0 4px; }
233
+ .collab-section .collab-text { margin-top: 0; }
234
+ .collab-section .collab-text + .collab-section-label { margin-top: 8px; }
235
+ .collab-files { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
236
+ .collab-file { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; padding: 4px 8px; border-radius: 4px; background: var(--bg-input); font-size: 13px; }
237
+ .collab-file-name { flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
238
+ .collab-file-size { font-size: 12px; color: var(--muted); }
239
+ .collab-file .text-btn { min-height: 24px; }
240
+ .collab-note { margin-top: 8px; font-size: 12px; color: var(--muted); line-height: 1.5; }
241
+ .collab-note.error, .collab-line.error { color: #f38b8d; }
242
+ .collab-line { font-size: 12px; color: var(--muted); line-height: 1.5; }
243
+ .collab-line:not(:empty) { margin-top: 8px; }
244
+ .collab-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
245
+ .collab-actions .text-btn.danger { color: #f38b8d; }
246
+ .collab-actions .text-btn.danger:hover:not(:disabled) { background: var(--danger); color: #fff; }
247
+ .collab-actions .text-btn:disabled, .collab-file .text-btn:disabled { opacity: 0.5; cursor: default; }
248
+ .collaboration-card .text-btn:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
249
+ @media (max-width: 640px) {
250
+ .collaboration-card { margin: 8px 8px 8px 16px; max-width: none; }
251
+ .collab-time { flex: 1 0 100%; margin-left: 0; }
252
+ .collab-actions .text-btn, .collab-file .text-btn { min-height: 44px; min-width: 44px; }
253
+ }
191
254
  .btn { border: 0; border-radius: 4px; padding: 6px 14px; font-weight: 600; font-size: 14px; }
192
255
  .btn.ok { background: var(--ok); color: #fff; }
193
256
  .btn.neutral { background: #4e5058; color: #fff; }
@@ -204,14 +267,35 @@ button { font: inherit; color: inherit; cursor: pointer; }
204
267
  .transition-status { flex: none; color: var(--warn); font-size: 13px; font-weight: 600; }
205
268
  .composer.transitioning { opacity: 0.72; }
206
269
  .composer :disabled { cursor: default; }
207
- .slash-menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 10; max-height: min(360px, 45vh); overflow-y: auto; background: #2b2d31; border: 1px solid #4e5058; border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); padding: 4px; }
208
- .slash-menu[hidden] { display: none; }
209
- .slash-item { display: flex; align-items: baseline; gap: 10px; padding: 7px 10px; border-radius: 6px; cursor: pointer; }
210
- .slash-item.active { background: #404249; }
211
- .slash-name { flex: none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--text); }
212
- .slash-desc { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #949ba4; }
213
- .slash-src { flex: none; font-size: 11px; color: #949ba4; border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; }
214
- .slash-note { padding: 7px 10px; font-size: 12px; color: #949ba4; }
270
+ /* The slash menu and the @ file menu share one idiom: panel, rows, active row, names, descriptions and notes. */
271
+ .slash-menu, .host-file-menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px + var(--menu-lift, 0px)); z-index: 10; max-height: min(360px, 45vh, var(--menu-room, 360px)); overflow-y: auto; background: #2b2d31; border: 1px solid #4e5058; border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); padding: 4px; }
272
+ .slash-menu[hidden], .host-file-menu[hidden] { display: none; }
273
+ .slash-item, .host-file-option { display: flex; align-items: baseline; gap: 10px; padding: 7px 10px; border-radius: 6px; cursor: pointer; }
274
+ .slash-item.active, .host-file-option[aria-selected="true"] { background: #404249; }
275
+ .slash-name, .host-file-option > span { flex: none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--text); }
276
+ .slash-desc, .host-file-option small { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #949ba4; }
277
+ .slash-src, .host-file-nav .text-btn { flex: none; font-size: 11px; color: #949ba4; border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; }
278
+ .slash-note, .host-file-status { padding: 7px 10px; font-size: 12px; color: #949ba4; }
279
+ /* The @ file menu adds its own layout: a toolbar row (title, search combobox, roots, parent, hidden files, paging),
280
+ the listbox that owns the bounded scroll, and a status line. Everything else comes from the shared rules above. */
281
+ .host-file-menu { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; }
282
+ .host-file-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 3px 6px 5px; }
283
+ .host-file-nav strong { flex: 1 1 auto; font-size: 11px; font-weight: 700; color: #949ba4; text-transform: uppercase; letter-spacing: 0.02em; }
284
+ .host-file-nav .text-btn { background: transparent; min-height: 24px; padding: 2px 8px; font-size: 12px; }
285
+ .host-file-nav .text-btn:hover:not(:disabled) { color: var(--text); background: #404249; }
286
+ .host-file-nav .text-btn:disabled { opacity: 0.5; cursor: default; }
287
+ .host-file-nav label { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; font-size: 12px; color: #949ba4; cursor: pointer; }
288
+ .host-file-nav input[type="checkbox"] { width: 14px; height: 14px; margin: 0; accent-color: var(--accent); }
289
+ .host-file-query-label { flex: 1 1 100%; }
290
+ .host-file-query-label input { flex: 1; min-width: 0; min-height: 28px; padding: 4px 8px; font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--text); background: var(--bg-input); border: 1px solid #4e5058; border-radius: 6px; }
291
+ .host-file-nav :focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
292
+ #host-file-list { overflow-y: auto; min-height: 0; overscroll-behavior: contain; }
293
+ .host-file-option > span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
294
+ .host-file-option small { flex: none; max-width: 50%; }
295
+ .host-file-option[aria-disabled="true"] { cursor: default; }
296
+ .host-file-option[aria-disabled="true"] > span { color: #949ba4; }
297
+ .host-file-status { flex: none; margin: 0 16px 8px; color: var(--text); overflow-wrap: anywhere; }
298
+ .host-file-menu .host-file-status { margin: 0; color: #949ba4; }
215
299
  .composer textarea { flex: 1; resize: none; background: transparent; border: 0; color: var(--text); font: inherit; font-size: 15px; line-height: 1.4; max-height: 200px; padding: 6px 4px; outline: none; }
216
300
  .mic, .send { border: 0; border-radius: 6px; padding: 6px 12px; background: #4e5058; color: #fff; font-size: 13px; font-weight: 600; }
217
301
  .send { background: var(--accent); }
@@ -344,7 +428,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
344
428
  .inbox-item-head { display: flex; align-items: center; gap: 8px; }
345
429
  .inbox-work { flex: 1; min-width: 0; font-size: 12px; font-weight: 700; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
346
430
  .inbox-chip { flex: none; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
347
- .chip-pending { background: rgba(240, 178, 50, 0.16); color: var(--warn); }
431
+ .chip-pending { background: var(--warn-soft); color: var(--warn); }
348
432
  .chip-needs_review { background: rgba(218, 55, 60, 0.18); color: #f38b8d; }
349
433
  .chip-submitted { background: rgba(88, 101, 242, 0.22); color: #a3acff; }
350
434
  .inbox-question { font-weight: 600; line-height: 1.4; white-space: pre-wrap; }
@@ -436,15 +520,33 @@ button { font: inherit; color: inherit; cursor: pointer; }
436
520
  .gc-all input { width: 16px; height: 16px; margin: 0; }
437
521
  .gc-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
438
522
  .gc-list:empty { display: none; }
523
+ /* Housekeeping todo 13: one group per role (상태 정리, 회수) with a count pill; a read-only row keeps the row frame but mutes its text. */
524
+ .gc-group { display: flex; flex-direction: column; gap: 6px; }
525
+ .gc-group[hidden] { display: none; }
526
+ .gc-group-head { display: flex; align-items: center; gap: 8px; padding: 4px 2px 0; font-size: 12px; font-weight: 600; color: var(--muted); }
527
+ .gc-count { font-size: 11px; font-weight: 600; min-width: 20px; padding: 1px 7px; border-radius: 10px; background: var(--bg-input); color: var(--text); text-align: center; font-variant-numeric: tabular-nums; }
528
+ .gc-rows { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
439
529
  .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; }
530
+ .gc-row.readonly { color: var(--muted); }
440
531
  .gc-row input { flex: none; width: 18px; height: 18px; margin: 0; }
532
+ .gc-row input:disabled { opacity: 0.4; cursor: default; }
533
+ .gc-mark { flex: none; width: 18px; height: 18px; display: grid; place-items: center; }
534
+ .gc-mark::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--warn); }
441
535
  .gc-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
536
+ .gc-line { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
442
537
  .gc-name, .gc-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
538
+ .gc-line .gc-name { flex: 0 1 auto; min-width: 0; }
539
+ .gc-badge { flex: none; font-size: 11px; font-weight: 600; line-height: 1.4; padding: 0 6px; border: 1px solid var(--warn); border-radius: 4px; color: var(--warn); white-space: nowrap; }
443
540
  .gc-sub { font-size: 12px; color: var(--muted); }
541
+ .gc-note { font-size: 12px; color: var(--warn); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
444
542
  .gc-message { margin: 0; font-size: 13px; color: var(--muted); }
445
543
  .gc-message:empty { display: none; }
446
544
  .gc-message.error { color: #f38b8d; }
447
- .gc-actions { display: flex; justify-content: flex-end; }
545
+ .gc-actions { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
546
+ .gc-audit { margin: 0; padding: 8px 10px; list-style: none; display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); background: var(--bg-card); border: 1px solid #4e5058; border-radius: 6px; }
547
+ .gc-audit[hidden] { display: none; }
548
+ .gc-audit li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
549
+ .gc-audit li.error { color: #f38b8d; }
448
550
  .settings.manual { z-index: 31; }
449
551
  .manual-card { width: min(100%, 760px); height: 100%; }
450
552
  .manual-status { margin: 0; padding: 12px 16px 0; font-size: 13px; color: var(--muted); }
@@ -599,19 +701,27 @@ button { font: inherit; color: inherit; cursor: pointer; }
599
701
  .agent-list li.stale .agent-stale-dot { display: inline; }
600
702
  .agent-list li .dot { margin-left: 0; }
601
703
  .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; }
704
+ .pe-clear:focus-visible, .pe-list-row:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
603
705
  .agent-head { display: flex; align-items: center; gap: 8px; min-width: 0; flex: none; }
604
706
  .agent-head[hidden] { display: none; }
605
707
  .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
708
  .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; }
709
+ /* The small "definition changed" pill of a list row (the agents dialog). It is not a control, so it has no hover state. */
710
+ .agent-stale { flex: none; border: 1px solid var(--warn); border-radius: 10px; background: var(--warn-soft); color: var(--warn); font: inherit; font-size: 12px; font-weight: 600; padding: 2px 8px; white-space: nowrap; }
711
+ /* Bug 283: the header's definition notice is a status text, not a button: the state line and, under it, an explanation visible without a mouse (a short form on phones). */
712
+ .agent-stale-status { display: flex; flex-direction: column; min-width: 0; color: var(--warn); font-size: 12px; font-weight: 600; line-height: 1.3; white-space: nowrap; }
713
+ .agent-stale-status[hidden] { display: none; }
714
+ .agent-stale-icon { margin-right: 4px; }
715
+ .agent-stale-hint { color: var(--muted); font-size: 11px; font-weight: 400; }
716
+ .agent-stale-hint.short { display: none; }
610
717
  .agents-card { width: min(100%, 640px); height: min(100%, 860px); }
611
718
  .agents-head { gap: 8px; }
612
719
  .agents-nav { display: flex; flex-wrap: wrap; gap: 6px; }
613
720
  .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); }
721
+ /* The save line and, after a save that changed a stale agent, its "지금 적용" button beside it (agent-ux todo 21). */
722
+ .agents-status-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 8px 16px 0; }
723
+ .agents-status-row:has(> .agents-status:empty):has(> .agents-apply-now[hidden]) { display: none; }
724
+ .agents-status { margin: 0; font-size: 12px; color: var(--muted); }
615
725
  .agents-status:empty { display: none; }
616
726
  .agents-status.error { color: #f38b8d; }
617
727
  .agents-status.ok { color: var(--ok); }
@@ -622,7 +732,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
622
732
  .pe-group { margin-top: 6px; justify-content: flex-start; flex-wrap: wrap; gap: 4px 8px; }
623
733
  .pe-group > span:first-child { flex: none; }
624
734
  .pe-group-help { font-weight: 400; text-transform: none; }
625
- .pe-note { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
735
+ .pe-note, .pe-help { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
626
736
  .pe-row { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: 4px 12px; align-items: start; }
627
737
  .pe-label { font-size: 13px; padding-top: 6px; }
628
738
  .pe-label small { display: block; font-size: 12px; color: var(--muted); font-weight: 400; }
@@ -633,11 +743,60 @@ button { font: inherit; color: inherit; cursor: pointer; }
633
743
  .pe-input[aria-invalid="true"] { border-color: var(--danger); }
634
744
  .pe-input.pe-number { flex: 0 1 140px; font-variant-numeric: tabular-nums; }
635
745
  .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; }
746
+ .pe-toggle { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; padding: 6px 0; }
638
747
  .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
748
  .pe-textarea.pe-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; min-height: 96px; }
640
749
  .pe-textarea.pe-body { min-height: 180px; }
750
+ /* agent-ux todo 9: the how-to-write box above the role instructions, a native disclosure (open while creating). */
751
+ .pe-guide { border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); padding: 6px 12px; font-size: 12px; line-height: 1.5; color: var(--muted); }
752
+ .pe-guide summary { cursor: pointer; padding: 4px 0; font-size: 13px; font-weight: 600; color: var(--text); border-radius: 4px; }
753
+ .pe-guide summary:hover { background: var(--bg-hover); }
754
+ .pe-guide summary:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
755
+ .pe-guide ul { margin: 4px 0 6px; padding-left: 18px; }
756
+ .pe-guide li + li { margin-top: 2px; }
757
+ .pe-guide b { color: var(--text); font-weight: 600; }
758
+ /* agent-ux todo 10: the allowed-skills check list (installed skills from the engine; a saved name outside it is tagged). */
759
+ .pe-skills { flex: 1 1 100%; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
760
+ .pe-skills-status { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; font-size: 12px; color: var(--muted); }
761
+ .pe-skills-status .text-btn { padding: 2px 8px; font-size: 12px; }
762
+ .pe-skill-list { display: flex; flex-direction: column; max-height: 240px; overflow-y: auto; padding: 4px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); }
763
+ .pe-skill { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 4px 6px; border-radius: 4px; font-size: 13px; cursor: pointer; }
764
+ .pe-skill:hover { background: var(--bg-hover); }
765
+ .pe-skill[hidden] { display: none; }
766
+ .pe-skill input { flex: none; width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; }
767
+ .pe-skill input:disabled { cursor: default; opacity: 0.6; }
768
+ .pe-skill-name { flex: none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
769
+ .pe-skill-desc { flex: 1; min-width: 0; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
770
+ .pe-skill-missing { flex: none; font-size: 11px; color: var(--warn); border: 1px solid var(--warn); border-radius: 4px; padding: 0 5px; white-space: nowrap; }
771
+ .pe-skill-search { flex: none; width: 100%; }
772
+ .pe-skills-empty { margin: 0; padding: 8px; font-size: 12px; color: var(--muted); }
773
+ .pe-skills-empty[hidden] { display: none; }
774
+ /* The collaboration targets list (292 C-10) reuses the skills list; the agent name is the row's text, its id the muted detail. */
775
+ .pe-target-name { flex: none; font-size: 13px; }
776
+ .pe-target.missing .pe-target-name { color: var(--muted); }
777
+ .pe-file-origin { flex: none; font-size: 11px; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; white-space: nowrap; }
778
+ .pe-file-origin.collaboration-copy { color: #a3acff; border-color: #5865f2; }
779
+ /* agent-ux todo 11: the create screen's draft status line and the list screen's drafts. */
780
+ .pe-draft-status { margin: 0; font-size: 12px; color: var(--muted); }
781
+ .pe-draft-status:empty { display: none; }
782
+ .pe-draft-status.ok { color: var(--ok); }
783
+ .pe-draft-status.warn { color: var(--warn); }
784
+ .pe-draft-list[hidden] { display: none; }
785
+ .pe-draft-list .pe-list-row { flex-wrap: wrap; }
786
+ .pe-draft-discard { flex: 1 1 100%; display: flex; flex-direction: column; gap: 8px; }
787
+ .pe-draft-discard[hidden] { display: none; }
788
+ /* agent-ux todo 12: the can/cannot box above the fields; two columns, one on the phone. */
789
+ .pe-capabilities { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); }
790
+ .pe-cap { min-width: 0; }
791
+ .pe-cap-line { grid-column: 1 / -1; margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
792
+ .pe-cap-title { margin: 0 0 4px; font-size: 12px; font-weight: 700; text-transform: uppercase; }
793
+ .pe-cap.can .pe-cap-title { color: var(--ok); }
794
+ .pe-cap.cannot .pe-cap-title { color: var(--danger); }
795
+ .pe-cap ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 2px; }
796
+ .pe-cap li { display: flex; gap: 6px; font-size: 12px; line-height: 1.5; color: var(--text); cursor: default; }
797
+ .pe-cap li::before { flex: none; font-weight: 700; }
798
+ .pe-cap.can li::before { content: "\2713"; color: var(--ok); }
799
+ .pe-cap.cannot li::before { content: "\2715"; color: var(--danger); }
641
800
  .pe-origin { flex: none; font-size: 11px; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; white-space: nowrap; }
642
801
  .pe-origin.agent, .pe-origin.defaults { color: #a3acff; border-color: #5865f2; }
643
802
  .pe-clear { background: none; border: 0; padding: 2px 4px; border-radius: 4px; color: #00a8fc; font-size: 12px; }
@@ -647,7 +806,7 @@ button { font: inherit; color: inherit; cursor: pointer; }
647
806
  .pe-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
648
807
  .pe-actions .pe-spacer { flex: 1; }
649
808
  .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; }
809
+ .pe-actions .text-btn.primary:disabled, .pe-actions .text-btn.danger:disabled, .pe-actions .text-btn:disabled, .pe-list-actions .text-btn:disabled { opacity: 0.5; cursor: default; }
651
810
  .text-btn.danger { background: var(--danger); color: #fff; }
652
811
  .pe-delete { display: flex; flex-direction: column; gap: 8px; }
653
812
  .pe-delete[hidden] { display: none; }
@@ -666,19 +825,96 @@ button { font: inherit; color: inherit; cursor: pointer; }
666
825
  .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
826
  .pe-file-name.directory { color: var(--muted); }
668
827
  .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
828
  @media (max-width: 640px) {
671
829
  .agent-head-name { max-width: 120px; }
672
830
  .pe-row { grid-template-columns: 1fr; }
673
831
  /* An agent conversation's header: the avatar and the agent block share the first row, the action buttons wrap below. */
674
832
  .chat-head:has(.agent-head:not([hidden])) { flex-wrap: wrap; }
675
833
  .agent-head { flex: 1 1 calc(100% - 44px); flex-wrap: wrap; }
834
+ /* The definition notice shares one line with its short explanation (bug 283). */
835
+ .agent-stale-status { flex-direction: row; flex-wrap: wrap; align-items: baseline; column-gap: 6px; }
836
+ .agent-stale-hint { display: none; }
837
+ .agent-stale-hint.short { display: inline; }
676
838
  .pe-label { padding-top: 0; }
677
- .agents-body .text-btn, .agent-stale, .pe-clear { min-height: 44px; }
839
+ .agents-body .text-btn, .pe-clear { min-height: 44px; }
840
+ .agents-apply-now { min-height: 44px; }
841
+ .pe-skill { min-height: 44px; }
842
+ .pe-capabilities { grid-template-columns: 1fr; }
678
843
  .pe-list-row { flex-wrap: wrap; }
679
844
  .pe-list-actions { width: 100%; justify-content: flex-start; }
680
845
  }
681
846
 
847
+ /* agent-ux todo 14: the upload zone of the knowledge files. A dashed box (the 1px dashed #4e5058 outline of .msg-ref) with the
848
+ styled 파일 고르기 .text-btn over a hidden file input (the browser's own control is never shown); files dragged over it turn
849
+ the outline --accent on --bg-hover, as the chat composer and the memo box take a drop. Each queued file is one row on
850
+ --bg-input: the monospace name and size of .pe-file, a 4px progress bar (#4e5058 track, --accent fill, --ok done,
851
+ --danger failed) and the state word with its icon, never colour alone. The phone keeps only the big 파일 고르기 button. */
852
+ .pe-upload-zone { display: flex; flex-direction: column; gap: 8px; padding: 12px; border: 1px dashed #4e5058; border-radius: 6px; background: var(--bg-card); transition: border-color 150ms ease, background 150ms ease; }
853
+ .pe-upload-zone.dragging { border-color: var(--accent); background: var(--bg-hover); }
854
+ .pe-upload-prompt { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px 8px; font-size: 13px; color: var(--muted); }
855
+ .pe-upload-pick { font-weight: 600; }
856
+ .pe-upload-rows { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
857
+ .pe-upload-rows[hidden] { display: none; }
858
+ .pe-upload-row { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-radius: 4px; background: var(--bg-input); }
859
+ .pe-upload-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; min-width: 0; }
860
+ .pe-upload-state { flex: none; font-size: 12px; color: var(--muted); white-space: nowrap; }
861
+ .pe-upload-state::before { font-weight: 700; margin-right: 4px; }
862
+ .pe-upload-state.waiting::before, .pe-upload-state.queued::before { content: "\25CB"; }
863
+ .pe-upload-state.uploading { color: #a3acff; }
864
+ .pe-upload-state.uploading::before { content: "\2191"; }
865
+ .pe-upload-state.done { color: var(--ok); }
866
+ .pe-upload-state.done::before { content: "\2713"; }
867
+ .pe-upload-state.failed { color: #f38b8d; white-space: normal; }
868
+ .pe-upload-state.failed::before { content: "\2715"; }
869
+ .pe-upload-retry { flex: none; padding: 2px 8px; font-size: 12px; }
870
+ .pe-upload-bar { height: 4px; border-radius: 2px; background: #4e5058; overflow: hidden; }
871
+ .pe-upload-fill { height: 100%; width: 0; border-radius: 2px; background: var(--accent); transition: width 150ms ease; }
872
+ .pe-upload-row.done .pe-upload-fill { background: var(--ok); }
873
+ .pe-upload-row.failed .pe-upload-bar { background: rgba(218, 55, 60, 0.35); }
874
+ .pe-upload-row.failed .pe-upload-fill { background: var(--danger); }
875
+ @media (prefers-reduced-motion: reduce) {
876
+ .pe-upload-zone, .pe-upload-fill { transition: none; }
877
+ }
878
+ /* agent-ux todo 15: the file section's three blocks (올린 자료, 결과물, 기록), each headed by its 13px name, its knowledge/ path as a
879
+ monospace tag (the #4e5058 outline of .pe-origin) and the info icon whose tooltip carries the detail; the 12px muted last
880
+ daily note line closes 기록. */
881
+ .pe-areas { display: flex; flex-direction: column; gap: 12px; }
882
+ .pe-area { display: flex; flex-direction: column; gap: 6px; }
883
+ .pe-area + .pe-area { border-top: 1px solid var(--border); padding-top: 10px; }
884
+ .pe-area-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; font-size: 13px; font-weight: 600; }
885
+ .pe-area-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; font-weight: 400; color: var(--muted); border: 1px solid #4e5058; border-radius: 4px; padding: 0 5px; }
886
+ .pe-last-daily { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
887
+ /* agent-ux todo 16: the compact 삭제 .text-btn.danger (trash glyph + word) at the right of a source/ or outputs/ row, the in-row
888
+ confirmation (the inbox-dismiss box of the agent delete) under that row, and the 삭제한 파일 disclosure (the .pe-guide
889
+ box) whose rows add 되살리기. */
890
+ .pe-file-delete { flex: none; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; font-size: 12px; }
891
+ .pe-file-confirm-row { list-style: none; padding: 2px 0 4px 8px; }
892
+ .pe-file-confirm { display: flex; flex-direction: column; gap: 8px; }
893
+ .pe-file-confirm .pe-actions .text-btn { padding: 4px 12px; }
894
+ .pe-file-trash { margin-top: 4px; }
895
+ .pe-file-trash summary { font-size: 13px; }
896
+ .pe-file-trash .pe-note { margin: 2px 0 6px; }
897
+ .pe-file-trash-list { margin-bottom: 6px; }
898
+ .pe-file-trash-row { gap: 8px; }
899
+ .pe-file-trash-row .text-btn { flex: none; padding: 2px 8px; font-size: 12px; }
900
+ /* agent-ux todo 17: the modified time of a file (12px muted, tabular) and the 내려받기 link drawn as the compact .text-btn. */
901
+ .pe-file-time { flex: none; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
902
+ .pe-file-download { flex: none; display: inline-flex; align-items: center; padding: 2px 8px; font-size: 12px; text-decoration: none; }
903
+ .pe-file-download:hover { text-decoration: none; }
904
+ @media (max-width: 640px) {
905
+ .pe-upload-hint { display: none; }
906
+ .pe-upload-pick { flex: 1 1 100%; min-height: 44px; }
907
+ .pe-upload-retry { min-height: 44px; }
908
+ .pe-file { flex-wrap: wrap; }
909
+ /* F3 V1: a saved row (올린 자료, 결과물, 삭제한 파일) gives its first line to the name alone - with the flex: 1 basis of 0
910
+ even alpha.txt was cut to alpha.t... - and wraps the time, size and buttons under it, the buttons at the right edge.
911
+ A name longer than the line still ellipsizes and keeps its full path on the row (title, read as aria-description).
912
+ The upload progress rows (.pe-upload-head), whose names were never cut, keep their one-line shape. */
913
+ .pe-file .pe-file-name { flex-basis: 100%; }
914
+ .pe-file .pe-file-size { margin-right: auto; }
915
+ .pe-file-delete, .pe-file-download, .pe-file-trash-row .text-btn { min-height: 44px; }
916
+ }
917
+
682
918
  /* Bug 90: the page-drawn tooltip (web/tooltip.js). */
683
919
  .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; }
684
920
  .zgw-tip[hidden] { display: none; }
@@ -740,3 +976,88 @@ button { font: inherit; color: inherit; cursor: pointer; }
740
976
  .info-tip::after { inset: -13px; }
741
977
  }
742
978
  .security-card .security-value { white-space: pre-wrap; overflow-wrap: anywhere; }
979
+
980
+ /* Plan agent-ux todo 23: the first-run tour (web/tour.js). Four shade panels dim the page and leave a hole over the
981
+ step's element, so only that element takes clicks; the ring frames the hole and the bubble below or above it carries
982
+ the sentence, the hint, n/7, 다음 (only where the real click is risky) and 건너뛰기. The phone, whose sidebar is never
983
+ shown, gets the sidebar as a drawer for that step only. Above every dialog (z-index 30), below the page tooltip (10000). */
984
+ .tour { position: fixed; inset: 0; z-index: 9500; pointer-events: none; }
985
+ .tour[hidden] { display: none; }
986
+ .tour-shade { position: fixed; background: rgba(0, 0, 0, 0.6); pointer-events: auto; }
987
+ .tour-shield { position: fixed; pointer-events: auto; cursor: not-allowed; }
988
+ .tour-shield[hidden] { display: none; }
989
+ .tour-ring { position: fixed; border: 2px solid var(--accent); border-radius: 8px; box-shadow: 0 0 0 4px rgba(88, 101, 242, 0.35); pointer-events: none; }
990
+ .tour-bubble { position: fixed; width: min(360px, calc(100vw - 16px)); padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-side); color: var(--text); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); pointer-events: auto; outline: none; }
991
+ .tour-text { margin: 0; font-size: 14px; line-height: 1.5; font-weight: 600; }
992
+ .tour-hint { margin: 4px 0 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
993
+ .tour-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
994
+ .tour-count { margin-right: auto; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
995
+ .tour-next { background: var(--accent); color: #fff; }
996
+ .tour-next:hover { background: var(--accent); filter: brightness(1.15); }
997
+ .tour-bubble .text-btn:focus-visible { outline: 2px solid #a3acff; outline-offset: 2px; }
998
+ /* F3 V2: the tour marks the body while it runs, so no page tooltip of the highlighted element (확인함 at step 4) is drawn
999
+ over the bubble. web/tooltip.js draws none while the mark is there; this rule hides one already on screen. */
1000
+ body[data-tip-quiet] .zgw-tip { display: none; }
1001
+ @media (max-width: 640px) {
1002
+ .tour-bubble .text-btn { min-height: 44px; }
1003
+ .app.tour-drawer .sidebar { display: flex; position: fixed; top: 0; left: 0; z-index: 9400; width: min(320px, 85vw); max-height: 60dvh; border-right: 1px solid var(--border); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); }
1004
+ .app.tour-drawer .work-status, .app.tour-drawer .sidebar-foot { display: none; }
1005
+ }
1006
+
1007
+ /* Plan 294 item 9: agent tool permissions. The inbox grant card (its rows in the inbox-meta look, the two design notes, the
1008
+ tab picker as option buttons on the #1e1f22 inset) and the 도구 권한 editor section (pe-list rows with a state chip, the
1009
+ settings switch, the inline scope editor in the inbox-dismiss box look). Colours are the existing tokens plus the #4e5058
1010
+ outline, the #a3acff accent and the #f38b8d error text the inbox and the editor already use. */
1011
+ .inbox-tool { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 10px; font-size: 13px; }
1012
+ .inbox-tool dt { color: var(--muted); font-weight: 600; }
1013
+ .inbox-tool dd { margin: 0; color: var(--text); overflow-wrap: anywhere; }
1014
+ .inbox-tool-note, .inbox-tool-restriction { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
1015
+ .inbox-tool-choices .btn { flex: 1 1 140px; }
1016
+ /* Held file read review (artifact hold): the unscanned-page warning is a warning word, the outcome reads as text. */
1017
+ .inbox-review-unscanned { color: var(--warn); }
1018
+ .inbox-review-outcome { color: var(--text); }
1019
+ .inbox-item.kind-artifact-review .inbox-review-line:empty { display: none; }
1020
+ .inbox-tool-targets { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px solid #4e5058; border-radius: 4px; background: #1e1f22; }
1021
+ .inbox-tool-targets-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 8px; }
1022
+ .inbox-tool-targets-label { font-size: 13px; font-weight: 600; }
1023
+ .inbox-tool-targets-empty { margin: 0; font-size: 12px; color: var(--muted); }
1024
+ .inbox-tool-target-list { margin-top: 0; }
1025
+ .inbox-tool-target { max-width: 100%; white-space: normal; text-align: left; }
1026
+ .inbox-tool-target-suggested { display: inline-block; margin-left: 6px; font-size: 11px; font-weight: 600; color: #a3acff; }
1027
+ .inbox-tool-target-url { display: block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; font-weight: 400; color: inherit; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
1028
+ .pe-tool-status { margin: 0; font-size: 12px; color: var(--muted); }
1029
+ .pe-tool-status:empty { display: none; }
1030
+ .pe-tool-status.error { color: #f38b8d; }
1031
+ .pe-tool-summary { font-weight: 600; }
1032
+ .pe-tool-pending { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
1033
+ .pe-tool-pending .link-btn { padding: 0 4px; font-size: 12px; }
1034
+ .pe-tool-row { flex-wrap: wrap; align-items: flex-start; }
1035
+ .pe-tool-main { flex: 1 1 240px; }
1036
+ .pe-tool-name { gap: 8px; white-space: normal; }
1037
+ .pe-tool-sub { white-space: normal; overflow-wrap: anywhere; }
1038
+ .pe-tool-meta { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
1039
+ .pe-tool-switch { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
1040
+ .pe-tool-reallow { flex: 1 1 100%; }
1041
+ .pe-tool-actions { gap: 8px; }
1042
+ .chip-allowed { background: var(--ok-soft); color: var(--ok); }
1043
+ .chip-applying { background: rgba(88, 101, 242, 0.22); color: #a3acff; }
1044
+ .chip-revoked, .chip-blocked { background: var(--bg-input); color: var(--muted); }
1045
+ .chip-suspended { background: var(--warn-soft); color: var(--warn); }
1046
+ .pe-tool-edit { flex: 1 1 100%; display: flex; flex-direction: column; gap: 8px; padding: 8px; border: 1px solid #4e5058; border-radius: 4px; }
1047
+ .pe-tool-edit-label { font-size: 12px; font-weight: 600; color: var(--muted); }
1048
+ .pe-tool-ops { display: flex; flex-direction: column; gap: 2px; }
1049
+ .pe-tool-op .switch-label { cursor: pointer; }
1050
+ .pe-tool-scope-field { display: flex; flex-direction: column; gap: 4px; }
1051
+ .pe-tool-diff { color: var(--text); }
1052
+ .pe-tool-diff.widen { color: var(--warn); }
1053
+ .pe-tool-all { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
1054
+ .pe-tool-all-confirm { align-self: stretch; }
1055
+ @media (max-width: 640px) {
1056
+ .inbox-tool { grid-template-columns: 1fr; gap: 0; }
1057
+ .inbox-tool dt { margin-top: 4px; }
1058
+ .inbox-tool-choices .btn, .inbox-tool-target, .inbox-tool-targets .text-btn { min-height: 44px; }
1059
+ .inbox-review-hide { min-height: 44px; }
1060
+ .pe-tool-row .text-btn, .pe-tool-edit .text-btn, .pe-tool-retry, .pe-tool-pending button { min-height: 44px; }
1061
+ .pe-tool-actions { width: 100%; justify-content: flex-start; }
1062
+ .pe-tool-switch { min-height: 44px; }
1063
+ }
package/web/tooltip.d.ts CHANGED
@@ -8,9 +8,11 @@ export type TipElement = {
8
8
  export const TIP_GAP: number;
9
9
  export const TIP_MARGIN: number;
10
10
  export const TIP_ID: string;
11
+ export const TIP_QUIET_ATTR: string;
11
12
  export function placeTip(anchor: Box, tip: { width: number; height: number }, view: { width: number; height: number }): { left: number; top: number; side: "above" | "below" };
12
13
  export function adoptTitle(el: Pick<TipElement, "getAttribute" | "setAttribute" | "removeAttribute" | "hasAttribute">): void;
13
14
  export function tipText(el: TipElement): string;
15
+ export function tipQuiet(body: Pick<TipElement, "hasAttribute">): boolean;
14
16
  export function addId(list: string | null, id: string): string;
15
17
  export function removeId(list: string | null, id: string): string;
16
18
  export function installTooltips(doc: Document, win: Window & typeof globalThis): void;
package/web/tooltip.js CHANGED
@@ -9,6 +9,8 @@
9
9
  export const TIP_GAP = 6;
10
10
  export const TIP_MARGIN = 8;
11
11
  export const TIP_ID = "zgw-tip";
12
+ // F3 V2: the mark the first-run tour (web/tour.js) writes on the body while it runs.
13
+ export const TIP_QUIET_ATTR = "data-tip-quiet";
12
14
  const HOVER_DELAY_MS = 250;
13
15
  const SELECTOR = "[data-tip]";
14
16
 
@@ -35,6 +37,15 @@ export function tipText(el) {
35
37
  return el.hasAttribute("data-tip-keep") ? el.getAttribute("data-tip") || "" : "";
36
38
  }
37
39
 
40
+ /**
41
+ * F3 V2: while the page runs its own overlay - the first-run tour, which marks the body - no hover or focus tip is drawn,
42
+ * so the tip of the element the tour highlights (확인함 at step 4) never covers the tour's bubble. The tour removes the
43
+ * mark when it finishes or is skipped, and tips work again.
44
+ */
45
+ export function tipQuiet(body) {
46
+ return body.hasAttribute(TIP_QUIET_ATTR);
47
+ }
48
+
38
49
  export function addId(list, id) {
39
50
  const ids = (list ?? "").split(/\s+/).filter(Boolean);
40
51
  return ids.includes(id) ? ids.join(" ") : [...ids, id].join(" ");
@@ -78,6 +89,7 @@ export function installTooltips(doc, win) {
78
89
  tip.hidden = true;
79
90
  };
80
91
  const show = (el) => {
92
+ if (tipQuiet(doc.body)) return hide();
81
93
  const text = tipText(el);
82
94
  if (!text || !el.isConnected) return hide();
83
95
  if (owner !== el) hide();
package/web/tour.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ export type TourAdvance = "input" | "click" | "next";
2
+ export interface TourStep {
3
+ readonly id: string;
4
+ readonly target: string;
5
+ readonly advance: TourAdvance;
6
+ readonly hit?: string;
7
+ readonly drawer?: boolean;
8
+ readonly opens?: "agents";
9
+ readonly within?: string;
10
+ }
11
+ export interface TourCover {
12
+ readonly id: string;
13
+ readonly open: string;
14
+ readonly close: string;
15
+ }
16
+ export interface TourStorage {
17
+ getItem(key: string): string | null;
18
+ setItem(key: string, value: string): void;
19
+ }
20
+ export type TourReason = "running" | "finished" | "skipped";
21
+ export interface TourState {
22
+ readonly at: number;
23
+ readonly done: boolean;
24
+ readonly reason: TourReason;
25
+ }
26
+ export type Box = { left: number; top: number; right: number; bottom: number };
27
+ export type Size = { width: number; height: number };
28
+ export interface TourController {
29
+ readonly start: () => void;
30
+ readonly skip: () => void;
31
+ readonly active: () => boolean;
32
+ readonly step: () => number;
33
+ }
34
+ export const TOUR_DONE_KEY: string;
35
+ export const TOUR_ID: string;
36
+ export const HOLE_PAD: number;
37
+ export const BUBBLE_GAP: number;
38
+ export const BUBBLE_MARGIN: number;
39
+ export const TOUR_STEPS: readonly TourStep[];
40
+ export const COVERS: readonly TourCover[];
41
+ export function readTourDone(storage: Pick<TourStorage, "getItem">): boolean;
42
+ export function writeTourDone(storage: Pick<TourStorage, "setItem">): void;
43
+ export function autoStartWanted(storage: Pick<TourStorage, "getItem">): boolean;
44
+ export function startState(): TourState;
45
+ export function tourTransition(state: TourState, action: "advance" | "skip", count?: number): TourState;
46
+ export function stepLabel(at: number, count?: number): string;
47
+ export function advanceMode(step: Pick<TourStep, "advance">, hasHit: boolean): TourAdvance;
48
+ export function hintKey(mode: TourAdvance): string;
49
+ export type TourKeyPlace = "shielded" | "input" | "row" | "other";
50
+ export type TourKeyAction = "skip" | "tab" | "block" | "activate";
51
+ export function keyAction(mode: TourAdvance, key: string, place: TourKeyPlace, shift?: boolean): TourKeyAction | null;
52
+ export function cycleIndex(length: number, at: number, shift: boolean): number;
53
+ export function subjectName(name: string, lang?: string): string;
54
+ export function holeBox(rects: readonly Box[], view: Size, pad?: number): Box | null;
55
+ export function shadeRects(hole: Box, view: Size): { top: Box; right: Box; bottom: Box; left: Box };
56
+ export function placeBubble(hole: Box, size: Size, view: Size): { left: number; top: number; side: "below" | "above" };
57
+ export function installTour(doc: Document, win: Window & typeof globalThis, storage: TourStorage): TourController;