zigmini 1.0.0-beta.2 → 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.
- package/README.ko.md +36 -3
- package/README.md +36 -3
- package/assets/agent/gateway-request-rules.md +1 -1
- package/assets/agent/gateway-rules.md +6 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/assets/document-reader/document-reader +0 -0
- package/assets/document-reader/host-selector +0 -0
- package/assets/document-reader/manifest.json +27 -0
- package/docs/manual.ko.md +120 -11
- package/docs/manual.md +123 -12
- package/host/document-reader/DocumentRead.swift +370 -0
- package/host/document-reader/HostSelector.swift +192 -0
- package/host/document-reader/Protocol.swift +647 -0
- package/host/document-reader/main.swift +22 -0
- package/host/omo-bridge/agent-aside-tool.ts +119 -0
- package/host/omo-bridge/agent-aside.ts +522 -0
- package/host/omo-bridge/agent-collaboration-files.ts +282 -0
- package/host/omo-bridge/agent-collaboration.ts +385 -0
- package/host/omo-bridge/agent-control.ts +400 -28
- package/host/omo-bridge/agent-execution.sb +42 -0
- package/host/omo-bridge/agent-execution.ts +158 -0
- package/host/omo-bridge/agent-tool-adapters.ts +388 -0
- package/host/omo-bridge/agent-tool-permissions.ts +96 -0
- package/host/omo-bridge/aside-tabs.ts +41 -0
- package/host/omo-bridge/attachment-read.ts +100 -0
- package/host/omo-bridge/bridge.ts +10 -0
- package/host/omo-bridge/managed-call.ts +46 -0
- package/host/omo-bridge/task-control.ts +39 -5
- package/host/omo-check/patches/attachments-293/README.md +138 -0
- package/host/omo-check/patches/attachments-293/document-content.js +234 -0
- package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
- package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
- package/host/omo-check/patches/omo-5.1.51-chatgpt-terra.patch +21 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +44 -6
- package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
- package/package.json +5 -3
- package/src/agent/api.ts +345 -20
- package/src/agent/artifacts.ts +196 -0
- package/src/agent/aside-target-routes.ts +369 -0
- package/src/agent/aside-targets.ts +215 -0
- package/src/agent/collaboration-admission.ts +47 -0
- package/src/agent/collaboration-read.ts +117 -0
- package/src/agent/collaboration-session.ts +196 -0
- package/src/agent/collaboration-types.ts +258 -0
- package/src/agent/collaboration.ts +399 -0
- package/src/agent/definition.ts +65 -3
- package/src/agent/fields.ts +9 -3
- package/src/agent/file-core.ts +325 -0
- package/src/agent/files.ts +87 -8
- package/src/agent/list.ts +3 -3
- package/src/agent/managed-call.ts +144 -0
- package/src/agent/tool-catalog.ts +184 -0
- package/src/agent/tool-delivery.ts +359 -0
- package/src/agent/tool-permission-types.ts +81 -0
- package/src/agent/tool-permissions.ts +1366 -0
- package/src/attachments/api.ts +161 -0
- package/src/attachments/document-reader.ts +325 -0
- package/src/attachments/host-selector.ts +410 -0
- package/src/attachments/managed-binding.ts +83 -0
- package/src/attachments/prepare.ts +214 -0
- package/src/attachments/selector-port.ts +28 -0
- package/src/attachments/service.ts +729 -0
- package/src/attachments/store.ts +698 -0
- package/src/attachments/types.ts +123 -0
- package/src/cli/doctor.ts +30 -10
- package/src/decisions.ts +109 -10
- package/src/engine/apply-patch.ts +21 -0
- package/src/engine/compat.json +2 -1
- package/src/engine/install.ts +45 -4
- package/src/engine/sandbox-cell.ts +2 -0
- package/src/gc.ts +33 -225
- package/src/housekeeping/api.ts +163 -0
- package/src/housekeeping/audit.ts +61 -0
- package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
- package/src/housekeeping/collectors/agent-drafts.ts +43 -0
- package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
- package/src/housekeeping/collectors/agent-trash.ts +28 -0
- package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
- package/src/housekeeping/collectors/db-backup.ts +25 -0
- package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
- package/src/housekeeping/collectors/docker-image.ts +135 -0
- package/src/housekeeping/collectors/omo-version.ts +32 -0
- package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
- package/src/housekeeping/collectors/reconcile-work.ts +55 -0
- package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
- package/src/housekeeping/collectors/shared.ts +149 -0
- package/src/housekeeping/collectors/temp-dir.ts +28 -0
- package/src/housekeeping/collectors/update-file.ts +25 -0
- package/src/housekeeping/collectors/worktree.ts +193 -0
- package/src/housekeeping/engine.ts +125 -0
- package/src/housekeeping/reconcile/work.ts +130 -0
- package/src/housekeeping/registry.ts +63 -0
- package/src/housekeeping/trash.ts +59 -0
- package/src/housekeeping/types.ts +31 -0
- package/src/hub.ts +1095 -41
- package/src/main.ts +230 -31
- package/src/message-references.ts +14 -5
- package/src/profile-prompt.ts +13 -1
- package/src/profile.ts +13 -5
- package/src/protocol.ts +41 -6
- package/src/security-allow.ts +79 -2
- package/src/security-check.ts +1 -1
- package/src/store.ts +1946 -23
- package/src/test-crash.ts +20 -0
- package/src/usage.ts +18 -7
- package/src/work-watch.ts +55 -7
- package/src/work.ts +192 -3
- package/web/DESIGN.md +35 -1
- package/web/agents.d.ts +189 -12
- package/web/agents.js +898 -39
- package/web/app.js +2634 -210
- package/web/attachments.d.ts +91 -0
- package/web/attachments.js +422 -0
- package/web/collaboration.d.ts +87 -0
- package/web/collaboration.js +290 -0
- package/web/decisions.d.ts +39 -2
- package/web/decisions.js +354 -2
- package/web/gc.d.ts +26 -5
- package/web/gc.js +113 -13
- package/web/i18n/agents.d.ts +4 -0
- package/web/i18n/agents.js +200 -8
- package/web/i18n/attachments.js +78 -0
- package/web/i18n/decisions.d.ts +4 -0
- package/web/i18n/decisions.js +68 -0
- package/web/i18n/gc.d.ts +4 -0
- package/web/i18n/gc.js +184 -2
- package/web/i18n/main.js +80 -8
- package/web/i18n/tour.d.ts +4 -0
- package/web/i18n/tour.js +37 -0
- package/web/index.html +20 -3
- package/web/profile.d.ts +2 -2
- package/web/profile.js +16 -10
- package/web/style.css +342 -21
- package/web/tooltip.d.ts +2 -0
- package/web/tooltip.js +12 -0
- package/web/tour.d.ts +57 -0
- package/web/tour.js +450 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Agent collaboration on the web (agent-collaboration-292 task 11, contract C-10). Only decisions live here, with no
|
|
2
|
+
// DOM, so test/unit/agent-collaboration-web.test.ts can exercise them: the per-conversation card state built from the
|
|
3
|
+
// stored `message.collaboration` / `agent.collaboration.*` events and the owner listing, the card view model (who asked
|
|
4
|
+
// whom, kind, state word, result, files, which actions apply), the owner requests (cancel, download, listing) and what
|
|
5
|
+
// their answers mean, the directional targets editor options and the source-file origin labels. app.js draws from these.
|
|
6
|
+
import { formatBytes } from "./gc.js";
|
|
7
|
+
import strings from "./i18n/agents.js";
|
|
8
|
+
import { registerStrings, t } from "./i18n.js";
|
|
9
|
+
|
|
10
|
+
registerStrings(strings);
|
|
11
|
+
|
|
12
|
+
export const REQUESTS_PATH = "/api/agent-collaboration/requests";
|
|
13
|
+
export const ARTIFACTS_PATH = "/api/agent-collaboration/artifacts";
|
|
14
|
+
/** The gateway's request states (src/store.ts agent_requests CHECK); anything else is shown as unconfirmed, never as done. */
|
|
15
|
+
export const REQUEST_STATES = Object.freeze(["queued", "running", "waiting_peer", "waiting_owner", "result_preparing", "completed", "failed", "interrupted", "cancel_requested", "cancelled", "unresolved_delivery", "version_conflict"]);
|
|
16
|
+
/** A request in one of these states never changes again; everything else may still move, including cancel_requested. */
|
|
17
|
+
export const TERMINAL_STATES = Object.freeze(["completed", "failed", "interrupted", "cancelled", "version_conflict"]);
|
|
18
|
+
const UNCERTAIN_STATES = Object.freeze(["unresolved_delivery", "cancel_requested", "version_conflict"]);
|
|
19
|
+
const KINDS = Object.freeze(["task", "question"]);
|
|
20
|
+
|
|
21
|
+
const isText = (value) => typeof value === "string";
|
|
22
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
|
|
24
|
+
export function isTerminal(state) {
|
|
25
|
+
return TERMINAL_STATES.includes(state);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The cards of one conversation: a new empty state (one per shown conversation). */
|
|
29
|
+
export function emptyCards() {
|
|
30
|
+
return new Map();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function blank(requestId) {
|
|
34
|
+
return { requestId, rootId: null, kind: null, text: null, files: [], actor: null, targetAgent: null, outgoing: null, seq: null, ts: null,
|
|
35
|
+
state: null, revision: null, resultId: null, result: null, replied: false, blocked: null, deliveryUnknown: false, listed: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function manifestRows(manifest) {
|
|
39
|
+
if (!Array.isArray(manifest)) return [];
|
|
40
|
+
return manifest.filter((row) => record(row) && isText(row.id)).map((row) => ({
|
|
41
|
+
artifactId: row.id, name: isText(row.originalName) && row.originalName ? row.originalName : row.id,
|
|
42
|
+
bytes: typeof row.bytes === "number" ? row.bytes : null, sha256: isText(row.sha256) ? row.sha256 : null, mediaType: isText(row.mediaType) ? row.mediaType : null,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One stored or live event folded into the cards. Returns the next map (the given one is not changed), the request the
|
|
48
|
+
* event touched, and what happened to its card: "created" (its first appearance, drawn at this event), "updated" (an
|
|
49
|
+
* existing card changed) or null (nothing to draw: an identical replay, or an event without a request). A notice is an
|
|
50
|
+
* event that names no request (blocked input, waiting for the owner's answer) and is shown as one system line.
|
|
51
|
+
*/
|
|
52
|
+
export function applyCollaborationEvent(cards, event) {
|
|
53
|
+
const next = new Map(cards);
|
|
54
|
+
const data = record(event?.data) ? event.data : {};
|
|
55
|
+
const requestId = isText(data.requestId) ? data.requestId : null;
|
|
56
|
+
const seq = typeof event?.seq === "number" ? event.seq : null;
|
|
57
|
+
const ts = isText(event?.ts) ? event.ts : null;
|
|
58
|
+
const none = { cards, requestId: null, change: null, notice: null };
|
|
59
|
+
switch (event?.type) {
|
|
60
|
+
case "message.collaboration": {
|
|
61
|
+
if (!requestId || (data.direction !== "request" && data.direction !== "reply")) return none;
|
|
62
|
+
const prior = next.get(requestId) ?? blank(requestId);
|
|
63
|
+
let card = { ...prior };
|
|
64
|
+
if (data.direction === "request") {
|
|
65
|
+
if (prior.text !== null && prior.seq !== null) return none;
|
|
66
|
+
card = { ...card, text: isText(data.text) ? data.text : "", files: Array.isArray(data.files) ? data.files.filter(isText) : [],
|
|
67
|
+
actor: record(data.actor) ? { kind: data.actor.kind === "main" ? "main" : "agent", id: isText(data.actor.id) ? data.actor.id : null } : card.actor,
|
|
68
|
+
outgoing: data.outgoing === true, rootId: isText(data.rootId) ? data.rootId : card.rootId };
|
|
69
|
+
} else {
|
|
70
|
+
const resultId = isText(data.resultId) ? data.resultId : null;
|
|
71
|
+
if (data.outgoing === true) {
|
|
72
|
+
if (prior.replied) return none;
|
|
73
|
+
card = { ...card, replied: true, resultId: resultId ?? card.resultId, outgoing: prior.outgoing ?? false };
|
|
74
|
+
} else {
|
|
75
|
+
if (prior.result && prior.result.resultId === resultId) return none;
|
|
76
|
+
card = { ...card, resultId: resultId ?? card.resultId, outgoing: prior.outgoing ?? true, rootId: isText(data.rootId) ? data.rootId : card.rootId,
|
|
77
|
+
result: { resultId, text: isText(data.text) ? data.text : "", files: manifestRows(data.manifest) } };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const created = prior.seq === null;
|
|
81
|
+
if (created) card = { ...card, seq, ts };
|
|
82
|
+
next.set(requestId, card);
|
|
83
|
+
return { cards: next, requestId, change: created ? "created" : "updated", notice: null };
|
|
84
|
+
}
|
|
85
|
+
case "agent.collaboration.status": {
|
|
86
|
+
if (!requestId || !isText(data.state)) return none;
|
|
87
|
+
const prior = next.get(requestId) ?? blank(requestId);
|
|
88
|
+
const revision = typeof data.revision === "number" ? data.revision : null;
|
|
89
|
+
if (prior.revision !== null && revision !== null && revision <= prior.revision) return none;
|
|
90
|
+
if (prior.state === data.state && prior.revision === revision) return none;
|
|
91
|
+
next.set(requestId, { ...prior, state: data.state, revision: revision ?? prior.revision });
|
|
92
|
+
return { cards: next, requestId, change: prior.seq === null ? null : "updated", notice: null };
|
|
93
|
+
}
|
|
94
|
+
case "agent.collaboration.blocked": {
|
|
95
|
+
const code = isText(data.code) ? data.code : "unknown";
|
|
96
|
+
if (!requestId) return { cards, requestId: null, change: null, notice: t("collab.notice.blocked", { code }) };
|
|
97
|
+
const prior = next.get(requestId) ?? blank(requestId);
|
|
98
|
+
if (prior.blocked === code) return none;
|
|
99
|
+
next.set(requestId, { ...prior, blocked: code });
|
|
100
|
+
return { cards: next, requestId, change: prior.seq === null ? null : "updated", notice: null };
|
|
101
|
+
}
|
|
102
|
+
case "agent.collaboration.waiting_owner":
|
|
103
|
+
return { cards, requestId: null, change: null, notice: t("collab.notice.waitingOwner") };
|
|
104
|
+
case "agent.collaboration.delivery_unknown": {
|
|
105
|
+
if (!requestId) return { cards, requestId: null, change: null, notice: t("collab.notice.deliveryUnknown") };
|
|
106
|
+
const prior = next.get(requestId) ?? blank(requestId);
|
|
107
|
+
if (prior.deliveryUnknown) return none;
|
|
108
|
+
next.set(requestId, { ...prior, deliveryUnknown: true });
|
|
109
|
+
return { cards: next, requestId, change: prior.seq === null ? null : "updated", notice: null };
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
return none;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The gateway's C-4 fallback `notice {kind:"collaboration",requestId,direction}` repeats a collaboration message for apps
|
|
118
|
+
* that do not know it. The web draws that message as its card, so it never draws the same request/direction notice too.
|
|
119
|
+
*/
|
|
120
|
+
export function isCollaborationFallback(event) {
|
|
121
|
+
const data = record(event?.data) ? event.data : {};
|
|
122
|
+
return event?.type === "notice" && data.kind === "collaboration" && isText(data.requestId) && (data.direction === "request" || data.direction === "reply");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The owner listing (GET /api/agent-collaboration/requests?conv=) is the gateway's current truth for kind, state,
|
|
127
|
+
* revision and target agent of every request this conversation asked or received; the stored events alone leave the
|
|
128
|
+
* receiving side without state changes and a sent card without its recipient. Returns the next map and the ids of the
|
|
129
|
+
* drawn cards whose view changed.
|
|
130
|
+
*/
|
|
131
|
+
export function mergeRequestListing(cards, listing) {
|
|
132
|
+
const rows = Array.isArray(listing?.requests) ? listing.requests : [];
|
|
133
|
+
const next = new Map(cards);
|
|
134
|
+
const changed = [];
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
if (!record(row) || !isText(row.requestId)) continue;
|
|
137
|
+
const prior = next.get(row.requestId) ?? blank(row.requestId);
|
|
138
|
+
const card = { ...prior, listed: true,
|
|
139
|
+
kind: KINDS.includes(row.kind) ? row.kind : prior.kind,
|
|
140
|
+
state: isText(row.state) ? row.state : prior.state,
|
|
141
|
+
revision: typeof row.revision === "number" ? row.revision : prior.revision,
|
|
142
|
+
resultId: isText(row.resultId) ? row.resultId : prior.resultId,
|
|
143
|
+
rootId: isText(row.rootId) ? row.rootId : prior.rootId,
|
|
144
|
+
targetAgent: isText(row.targetAgent) && row.targetAgent ? row.targetAgent : prior.targetAgent,
|
|
145
|
+
text: prior.text ?? (isText(row.text) ? row.text : null),
|
|
146
|
+
files: prior.files.length ? prior.files : Array.isArray(row.files) ? row.files.filter(isText) : [] };
|
|
147
|
+
next.set(row.requestId, card);
|
|
148
|
+
if (prior.seq !== null && (prior.kind !== card.kind || prior.state !== card.state || prior.revision !== card.revision || prior.resultId !== card.resultId || prior.text !== card.text || prior.targetAgent !== card.targetAgent || !prior.listed)) changed.push(row.requestId);
|
|
149
|
+
}
|
|
150
|
+
return { cards: next, changed };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The visual tone of a state word: done, still moving, needs attention, or ended without a result. Never colour alone. */
|
|
154
|
+
export function stateTone(state) {
|
|
155
|
+
if (state === "completed") return "ok";
|
|
156
|
+
if (state === null || UNCERTAIN_STATES.includes(state) || state === "waiting_owner") return "warn";
|
|
157
|
+
if (isTerminal(state)) return "danger";
|
|
158
|
+
return "busy";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function stateLabel(state) {
|
|
162
|
+
return t(REQUEST_STATES.includes(state) ? `collab.state.${state}` : "collab.state.unconfirmed");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function agentName(agents, id) {
|
|
166
|
+
const row = Array.isArray(agents) ? agents.find((agent) => agent?.id === id) : null;
|
|
167
|
+
return isText(row?.name) && row.name ? row.name : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* What one card shows. `ctx`: the agent list (names by id), the open conversation's agent id (null in main), main's
|
|
172
|
+
* name. The sender of an incoming request is its actor; an outgoing request's recipient is not in the stored event, so
|
|
173
|
+
* it reads as the other agent until the owner listing names it (F3 D5).
|
|
174
|
+
*/
|
|
175
|
+
export function cardView(card, ctx) {
|
|
176
|
+
const self = ctx.agentId === null ? ctx.mainName : agentName(ctx.agents, ctx.agentId) ?? t("collab.party.deleted");
|
|
177
|
+
const actor = card.actor;
|
|
178
|
+
const actorName = actor === null ? null : actor.kind === "main" ? ctx.mainName : agentName(ctx.agents, actor.id) ?? t("collab.party.deleted");
|
|
179
|
+
const outgoing = card.outgoing === true;
|
|
180
|
+
const from = outgoing ? self : actorName ?? t("collab.party.other");
|
|
181
|
+
const targetName = card.targetAgent === null ? null : agentName(ctx.agents, card.targetAgent) ?? t("collab.party.deleted");
|
|
182
|
+
const to = outgoing ? targetName ?? t("collab.party.other") : self;
|
|
183
|
+
const state = card.state;
|
|
184
|
+
const uncertain = state === null || UNCERTAIN_STATES.includes(state) || card.deliveryUnknown;
|
|
185
|
+
const notes = [];
|
|
186
|
+
if (card.deliveryUnknown) notes.push({ text: t("collab.note.deliveryUnknown"), error: true });
|
|
187
|
+
if (card.blocked !== null) notes.push({ text: t("collab.note.blocked", { code: card.blocked }), error: true });
|
|
188
|
+
if (state === "waiting_owner") notes.push({ text: t("collab.note.waitingOwner"), error: false });
|
|
189
|
+
if (state === "version_conflict") notes.push({ text: t("collab.note.versionConflict"), error: true });
|
|
190
|
+
if (state === "unresolved_delivery") notes.push({ text: t("collab.note.unresolvedDelivery"), error: true });
|
|
191
|
+
if (state === null) notes.push({ text: t("collab.note.unconfirmed"), error: false });
|
|
192
|
+
return {
|
|
193
|
+
requestId: card.requestId, outgoing, from, to, ts: card.ts,
|
|
194
|
+
title: t(outgoing ? "collab.title.sent" : "collab.title.received"),
|
|
195
|
+
kind: card.kind, kindLabel: card.kind === null ? null : t(`collab.kind.${card.kind}`),
|
|
196
|
+
state, stateLabel: stateLabel(state), tone: stateTone(state), uncertain,
|
|
197
|
+
text: card.text ?? "",
|
|
198
|
+
inputs: card.files.map((artifactId, index) => ({ artifactId, name: t("collab.file.input", { n: index + 1 }), bytes: null, sha256: null })),
|
|
199
|
+
result: card.result === null ? null : { text: card.result.text, files: card.result.files.map((file) => ({ ...file, size: file.bytes === null ? "" : formatBytes(file.bytes) })) },
|
|
200
|
+
replied: card.replied,
|
|
201
|
+
cancellable: state !== null && !isTerminal(state),
|
|
202
|
+
notes,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The cancel request for a card: the gateway compares the revision the screen last saw, so a card without one cannot ask yet. */
|
|
207
|
+
export function cancelRequest(card) {
|
|
208
|
+
if (!card || typeof card.revision !== "number") return null;
|
|
209
|
+
return { method: "POST", path: `${REQUESTS_PATH}/${encodeURIComponent(card.requestId)}/cancel`, headers: { "content-type": "application/json" }, body: JSON.stringify({ expectedRevision: card.revision }) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function listingRequest(conv) {
|
|
213
|
+
return { method: "GET", path: `${REQUESTS_PATH}?conv=${encodeURIComponent(conv)}` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function artifactPath(artifactId) {
|
|
217
|
+
return `${ARTIFACTS_PATH}/${encodeURIComponent(artifactId)}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* What a cancel answer means for the card and the line under it. 200 carries the gateway's receipt (state, revision);
|
|
222
|
+
* every refusal is a sentence, and the owner-helper refusal (503 owner_auth_unavailable) says the action is not
|
|
223
|
+
* available yet rather than pretending it worked.
|
|
224
|
+
*/
|
|
225
|
+
export function cancelOutcome(status, body) {
|
|
226
|
+
const code = isText(body?.error) ? body.error : null;
|
|
227
|
+
if (status === 200 && record(body) && isText(body.state)) {
|
|
228
|
+
const state = body.state, revision = typeof body.revision === "number" ? body.revision : null;
|
|
229
|
+
return { ok: true, state, revision, line: t(state === "cancelled" ? "collab.cancel.done" : state === "cancel_requested" ? "collab.cancel.requested" : "collab.cancel.state", { state: stateLabel(state) }) };
|
|
230
|
+
}
|
|
231
|
+
if (status === 0) return { ok: false, state: null, revision: null, line: t("collab.cancel.network") };
|
|
232
|
+
if (status === 503 && code === "owner_auth_unavailable") return { ok: false, state: null, revision: null, line: t("collab.cancel.unavailable") };
|
|
233
|
+
if (status === 401 || status === 403) return { ok: false, state: null, revision: null, line: t("collab.cancel.ownerRequired") };
|
|
234
|
+
if (status === 404) return { ok: false, state: null, revision: null, line: t("collab.cancel.notFound") };
|
|
235
|
+
if (status === 409) return { ok: false, state: null, revision: null, line: t("collab.cancel.conflict") };
|
|
236
|
+
return { ok: false, state: null, revision: null, line: t("collab.cancel.failed", { reason: code ?? `HTTP ${status}` }) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* What a download answer means. The bytes are saved only when the gateway's digest header equals the manifest's sha256
|
|
241
|
+
* (the header alone for an input file the manifest does not describe) and, where the browser could hash them (a secure
|
|
242
|
+
* context), the digest of what arrived equals it too; a mismatch is reported and nothing is saved.
|
|
243
|
+
*/
|
|
244
|
+
export function downloadOutcome(name, status, body, digests) {
|
|
245
|
+
const code = isText(body?.error) ? body.error : null;
|
|
246
|
+
if (status === 200) {
|
|
247
|
+
const reference = isText(digests.expected) ? digests.expected : digests.header;
|
|
248
|
+
const verified = isText(reference) && digests.header === reference && (digests.computed === null || digests.computed === reference);
|
|
249
|
+
return verified ? { ok: true, line: t("collab.download.done", { name }) } : { ok: false, line: t("collab.download.mismatch", { name }) };
|
|
250
|
+
}
|
|
251
|
+
if (status === 0) return { ok: false, line: t("collab.download.failed", { name, reason: t("collab.reason.network") }) };
|
|
252
|
+
if (status === 503 && code === "owner_auth_unavailable") return { ok: false, line: t("collab.download.unavailable") };
|
|
253
|
+
if (status === 401 || status === 403) return { ok: false, line: t("collab.download.failed", { name, reason: t("collab.reason.ownerRequired") }) };
|
|
254
|
+
if (status === 404) return { ok: false, line: t("collab.download.failed", { name, reason: t("collab.reason.notFound") }) };
|
|
255
|
+
if (status === 409) return { ok: false, line: t("collab.download.failed", { name, reason: t("collab.reason.changed") }) };
|
|
256
|
+
return { ok: false, line: t("collab.download.failed", { name, reason: code ?? `HTTP ${status}` }) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function listingOutcome(status, body) {
|
|
260
|
+
if (status === 200) return { ok: true, line: t("collab.recheck.done") };
|
|
261
|
+
const code = isText(body?.error) ? body.error : null;
|
|
262
|
+
return { ok: false, line: t("collab.recheck.failed", { reason: status === 0 ? t("collab.reason.network") : code ?? `HTTP ${status}` }) };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The directional targets check list (C-10): every other agent by name (never this agent itself), checked when its id is
|
|
267
|
+
* saved; a saved id the list does not carry (a deleted agent) stays checked and is marked missing, so the owner sees it
|
|
268
|
+
* and can uncheck it. Before the list is loaded only the saved ids are known.
|
|
269
|
+
*/
|
|
270
|
+
export function targetOptions(agents, selfId, saved) {
|
|
271
|
+
const savedIds = Array.isArray(saved) ? [...new Set(saved.filter((id) => isText(id) && id.trim()).map((id) => id.trim()))] : [];
|
|
272
|
+
if (!Array.isArray(agents)) return { loaded: false, options: savedIds.map((id) => ({ id, name: id, checked: true, missing: null })) };
|
|
273
|
+
const known = agents.filter((agent) => record(agent) && isText(agent.id) && agent.id !== selfId)
|
|
274
|
+
.map((agent) => ({ id: agent.id, name: isText(agent.name) && agent.name ? agent.name : agent.id, checked: savedIds.includes(agent.id), missing: false }))
|
|
275
|
+
.sort((a, b) => a.name.localeCompare(b.name, "ko"));
|
|
276
|
+
const missing = savedIds.filter((id) => !known.some((option) => option.id === id)).map((id) => ({ id, name: id, checked: true, missing: true }));
|
|
277
|
+
return { loaded: true, options: [...missing, ...known] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The origin tag of a source/ file: what the owner uploaded, or a copy received through collaboration; nothing for other files. */
|
|
281
|
+
export function fileOriginLabel(row) {
|
|
282
|
+
if (row?.origin === "collaboration-copy") return { origin: "collaboration-copy", text: t("agents.file.origin.copy"), tip: t("agents.file.origin.copy.tip") };
|
|
283
|
+
if (row?.origin === "user-upload") return { origin: "user-upload", text: t("agents.file.origin.upload"), tip: t("agents.file.origin.upload.tip") };
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The delete confirmation of a collaboration copy also says what deleting the copy does not undo. */
|
|
288
|
+
export function fileDeleteText(row) {
|
|
289
|
+
return row?.origin === "collaboration-copy" ? `${t("agents.file.delete.confirm")} ${t("agents.file.delete.copy")}` : t("agents.file.delete.confirm");
|
|
290
|
+
}
|
package/web/decisions.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
DecisionAnswerRequest, DecisionCountersInfo, DecisionInfo, DecisionResponse,
|
|
2
|
+
ArtifactReviewInfo, DecisionAnswerRequest, DecisionCountersInfo, DecisionInfo, DecisionResponse,
|
|
3
3
|
DecisionsPageData, DecisionUpdatedData,
|
|
4
4
|
} from "../src/protocol.ts";
|
|
5
|
+
import type { ArtifactReviewSummary } from "../src/security-allow.ts";
|
|
5
6
|
|
|
6
7
|
type AnswerMode = DecisionAnswerRequest["mode"];
|
|
7
8
|
type Answer = DecisionResponse | { readonly cancelled: true };
|
|
@@ -86,7 +87,7 @@ export function finishDetailLoad(
|
|
|
86
87
|
): DetailResult;
|
|
87
88
|
export function failDetailLoad(inbox: Inbox, id: string, generation: number): boolean;
|
|
88
89
|
export function presentation(decision: DecisionInfo): {
|
|
89
|
-
readonly kind: "terminal" | "submitted" | "live" | "resume" | "permission" | "recheck" | "security";
|
|
90
|
+
readonly kind: "terminal" | "submitted" | "live" | "resume" | "permission" | "recheck" | "security" | "artifact-review";
|
|
90
91
|
readonly mode: AnswerMode | null;
|
|
91
92
|
readonly form: boolean;
|
|
92
93
|
readonly recheck: boolean;
|
|
@@ -109,6 +110,8 @@ export function buildRequest(
|
|
|
109
110
|
| [mode: "request_permission" | "recheck", response: undefined, newId?: () => string]
|
|
110
111
|
): DecisionAnswerRequest | null;
|
|
111
112
|
export function interpretAnswerResponse(status: number, body: unknown): AnswerResult;
|
|
113
|
+
/** 294 C3: the answer of a tool card (grant, action, migration) carries the owner client headers. */
|
|
114
|
+
export function ownerAnswerRequired(decision: unknown): boolean;
|
|
112
115
|
export function noteConflict(inbox: Inbox, decision: DecisionInfo, latest: DecisionInfo | null | undefined): void;
|
|
113
116
|
export function reference(inbox: Inbox, decision: DecisionInfo): DraftReference | null;
|
|
114
117
|
export function isReviewRequired(inbox: Inbox, id: string): boolean;
|
|
@@ -127,3 +130,37 @@ export const WORK_STATUS_LABELS: Readonly<Record<
|
|
|
127
130
|
"submitted" | "running" | "waiting" | "waiting_input" | "completed" | "failed" | "cancelled" | "unknown", string
|
|
128
131
|
>>;
|
|
129
132
|
export const ROUTE_LABELS: Readonly<Record<NonNullable<DecisionInfo["answerRoute"]>, string>>;
|
|
133
|
+
|
|
134
|
+
/** Artifact review holds (agent-collaboration-292 owner gap): the owner card of a held files.read and its owner calls. */
|
|
135
|
+
export const ARTIFACT_REVIEW_PURPOSE: "collaboration_artifact_review";
|
|
136
|
+
export const ARTIFACT_HOLDS_PATH: "/api/agent-collaboration/holds";
|
|
137
|
+
type ReviewChoice = "allow" | "deny";
|
|
138
|
+
export function artifactReview(decision: Partial<DecisionInfo> | null | undefined): ArtifactReviewInfo | null;
|
|
139
|
+
export function isArtifactReview(decision: Partial<DecisionInfo> | null | undefined): boolean;
|
|
140
|
+
export function reviewFindingsText(summary: ArtifactReviewSummary | null | undefined): string;
|
|
141
|
+
export function reviewUnscannedText(summary: ArtifactReviewSummary | null | undefined): string | null;
|
|
142
|
+
export function reviewOutcomeText(review: { readonly state: string; readonly resumeState?: string | null }): string | null;
|
|
143
|
+
export function artifactReviewView(decision: DecisionInfo, options?: { readonly agentName?: (id: string) => string; readonly mainName?: string }): {
|
|
144
|
+
readonly holdId: string;
|
|
145
|
+
readonly revision: number;
|
|
146
|
+
readonly state: ArtifactReviewInfo["state"];
|
|
147
|
+
readonly resumeState: ArtifactReviewInfo["resumeState"];
|
|
148
|
+
readonly title: string;
|
|
149
|
+
readonly chip: string | null;
|
|
150
|
+
readonly rows: ReadonlyArray<{ readonly key: "agent" | "file" | "part" | "findings"; readonly label: string; readonly value: string }>;
|
|
151
|
+
readonly warning: string | null;
|
|
152
|
+
readonly question: string | null;
|
|
153
|
+
readonly outcome: string | null;
|
|
154
|
+
readonly pending: boolean;
|
|
155
|
+
readonly choices: ReadonlyArray<{ readonly choice: ReviewChoice; readonly label: string; readonly cls: "ok" | "danger" }>;
|
|
156
|
+
} | null;
|
|
157
|
+
export function artifactReviewRequest(decision: DecisionInfo, choice: ReviewChoice, newId?: () => string): {
|
|
158
|
+
readonly method: "POST"; readonly path: string; readonly headers: Readonly<Record<string, string>>; readonly body: string;
|
|
159
|
+
} | null;
|
|
160
|
+
export function artifactReviewOutcome(status: number, body: unknown): {
|
|
161
|
+
readonly ok: boolean;
|
|
162
|
+
readonly kind: "allowed" | "denied" | "cancelled" | "network" | "unavailable" | "owner" | "gone" | "conflict" | "failed";
|
|
163
|
+
readonly revision: number | null;
|
|
164
|
+
readonly resumeState: string | null;
|
|
165
|
+
readonly line: string | null;
|
|
166
|
+
};
|