@k2b/cloud 0.39.0 → 0.40.0
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.md +8 -0
- package/package.json +8 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +36 -15
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +2 -0
- package/src/ai/migrate.ts +11 -2
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +145 -1
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +227 -24
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +24 -2
- package/src/ai/types.ts +28 -3
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +120 -13
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +7 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +11 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +2 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/effects.css +198 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CapabilitySentencePart,
|
|
3
|
+
capabilityAskReasonText,
|
|
4
|
+
capabilityQuestionText,
|
|
5
|
+
capabilityReceiptParts,
|
|
6
|
+
capabilityReviewSentence,
|
|
7
|
+
capabilityReviewSentenceParts,
|
|
8
|
+
capabilitySentenceText,
|
|
9
|
+
} from "../../_internal/capability-sentences";
|
|
10
|
+
import type {
|
|
11
|
+
CapabilityActionCallReview,
|
|
12
|
+
CapabilityActionReceipt,
|
|
13
|
+
CapabilityActionReview,
|
|
14
|
+
CapabilityAskReason,
|
|
15
|
+
CapabilityConsequence,
|
|
16
|
+
CapabilityPreview,
|
|
17
|
+
CapabilitySemanticLink,
|
|
18
|
+
CapabilitySlot,
|
|
19
|
+
} from "../../contracts/capabilities";
|
|
20
|
+
import { cloudResourceHref } from "../../contracts/resource-href";
|
|
21
|
+
import {
|
|
22
|
+
type AiLegacyReviewRow,
|
|
23
|
+
type AiStoredActionReview,
|
|
24
|
+
isLegacyActionReview,
|
|
25
|
+
legacyActionReview,
|
|
26
|
+
storedActionReview,
|
|
27
|
+
} from "../action-review";
|
|
28
|
+
import type { AiTurnBlock } from "../protocol";
|
|
29
|
+
import { toolSubject } from "../tool-wording";
|
|
30
|
+
import { actionMessages } from "./action-messages";
|
|
31
|
+
import { aiToolIcon, displayToolName } from "./message-utils";
|
|
32
|
+
import { aiChatMessages } from "./messages";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One Action question and its outcome lines as data, the same for the chat, the approvals page, and Code Mode. A new
|
|
36
|
+
* review gives the app's sentence with typed chips, its preview, and its buttons; an older review is shown as its
|
|
37
|
+
* literal text. Pure, so the server render and the first browser render agree.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
41
|
+
|
|
42
|
+
export type ActionChip =
|
|
43
|
+
| { kind: "object" | "place"; title: string; href?: string; icon?: string; gone?: boolean }
|
|
44
|
+
| { kind: "person"; names: string[]; rest: number; more: string; locale: string }
|
|
45
|
+
| { kind: "time"; label: string; dateTime: string };
|
|
46
|
+
export type ActionTextPart = { kind: "text"; text: string } | { kind: "chip"; chip: ActionChip };
|
|
47
|
+
/** A sentence as text and chips. */
|
|
48
|
+
export type ActionText = ActionTextPart[];
|
|
49
|
+
|
|
50
|
+
export type ActionReason = { code: CapabilityAskReason; text: string; icon: string; tone: "neutral" | "danger" | "warning" };
|
|
51
|
+
|
|
52
|
+
export type ActionPreviewModel =
|
|
53
|
+
| {
|
|
54
|
+
kind: "review";
|
|
55
|
+
locale: string;
|
|
56
|
+
timeZone: string;
|
|
57
|
+
preview?: CapabilityPreview;
|
|
58
|
+
consequences: { text: ActionText; mark?: "added" | "removed" }[];
|
|
59
|
+
/** The rule sentence of a `rule` preview, with its chips. */
|
|
60
|
+
when?: ActionText;
|
|
61
|
+
/** Each `may` line of a `rule` preview. */
|
|
62
|
+
may?: { text: ActionText; mark?: "added" | "removed" }[];
|
|
63
|
+
}
|
|
64
|
+
| { kind: "legacy"; rows: AiLegacyReviewRow[]; markdown?: string; input?: unknown };
|
|
65
|
+
|
|
66
|
+
export type ActionCard = {
|
|
67
|
+
/** The language of the question; Cloud's words on the card use it too, so one card never mixes languages. */
|
|
68
|
+
locale: string;
|
|
69
|
+
app: { name: string; icon: string; accent?: string };
|
|
70
|
+
question: ActionText;
|
|
71
|
+
/** The plain question, for the accessible name of places that only list it. */
|
|
72
|
+
questionText: string;
|
|
73
|
+
reason: ActionReason;
|
|
74
|
+
preview: ActionPreviewModel | null;
|
|
75
|
+
/** Opens exactly this version of the thing. */
|
|
76
|
+
readMore?: string;
|
|
77
|
+
/** The review's other links, such as editing the draft, each with its words. */
|
|
78
|
+
links: { href: string; label: string }[];
|
|
79
|
+
/** Where the thing itself opens, for an outcome Cloud could not see. */
|
|
80
|
+
open?: string;
|
|
81
|
+
confirm: { label: string; variant: "ai" | "danger" };
|
|
82
|
+
decline: string;
|
|
83
|
+
forms: {
|
|
84
|
+
running: ActionText;
|
|
85
|
+
done: ActionText;
|
|
86
|
+
rejected: ActionText;
|
|
87
|
+
notRun: ActionText;
|
|
88
|
+
unknown: ActionText;
|
|
89
|
+
unknownStill: ActionText;
|
|
90
|
+
};
|
|
91
|
+
/** The thing the question is about, for "Your answer about … didn't arrive". */
|
|
92
|
+
subject: ActionText;
|
|
93
|
+
/** One short plain sentence, for a message that asks the Assistant again. */
|
|
94
|
+
short: string;
|
|
95
|
+
/** The app can settle an unknown outcome with its `check` Query. */
|
|
96
|
+
hasCheck: boolean;
|
|
97
|
+
/** The receipt text of an old review's success summary. */
|
|
98
|
+
summary?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const REASON_LOOK: Record<CapabilityAskReason, Omit<ActionReason, "code" | "text">> = {
|
|
102
|
+
sends: { icon: "ti ti-world", tone: "neutral" },
|
|
103
|
+
deletes: { icon: "ti ti-alert-triangle", tone: "danger" },
|
|
104
|
+
cannot_undo: { icon: "ti ti-alert-triangle", tone: "danger" },
|
|
105
|
+
involves_people: { icon: "ti ti-users-group", tone: "neutral" },
|
|
106
|
+
changes_access: { icon: "ti ti-users", tone: "neutral" },
|
|
107
|
+
keeps_working: { icon: "ti ti-bolt", tone: "neutral" },
|
|
108
|
+
sensitive: { icon: "ti ti-lock", tone: "neutral" },
|
|
109
|
+
app_asks: { icon: "ti ti-info-circle", tone: "neutral" },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const actionReason = (code: CapabilityAskReason, context: { locale: string; appName: string }): ActionReason => ({
|
|
113
|
+
code,
|
|
114
|
+
text: capabilityAskReasonText(code, context),
|
|
115
|
+
...REASON_LOOK[code],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type ActionCardContext = { locale: string; timeZone: string; app: { name: string; icon: string; accent?: string } };
|
|
119
|
+
|
|
120
|
+
const textPart = (text: string): ActionTextPart => ({ kind: "text", text });
|
|
121
|
+
const plainText = (text: string): ActionText => [textPart(text)];
|
|
122
|
+
|
|
123
|
+
/** Merges neighbouring text parts, so a sentence never splits a word between two text nodes. */
|
|
124
|
+
const merge = (parts: ActionText): ActionText =>
|
|
125
|
+
parts.reduce<ActionText>((all, part) => {
|
|
126
|
+
const last = all.at(-1);
|
|
127
|
+
if (part.kind === "text" && last?.kind === "text") all[all.length - 1] = textPart(last.text + part.text);
|
|
128
|
+
else if (part.kind !== "text" || part.text) all.push(part);
|
|
129
|
+
return all;
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const chipOf = (slot: CapabilitySlot, label: string, locale: string, app: ActionCardContext["app"]): ActionChip | null => {
|
|
133
|
+
switch (slot.kind) {
|
|
134
|
+
case "object":
|
|
135
|
+
case "place":
|
|
136
|
+
return {
|
|
137
|
+
kind: slot.kind,
|
|
138
|
+
title: slot.title,
|
|
139
|
+
...(slot.ref && !(slot.kind === "object" && slot.gone) ? { href: cloudResourceHref(slot.ref) } : {}),
|
|
140
|
+
icon: slot.icon ?? app.icon,
|
|
141
|
+
...(slot.kind === "object" && slot.gone ? { gone: true } : {}),
|
|
142
|
+
};
|
|
143
|
+
case "person": {
|
|
144
|
+
const shown = slot.people.slice(0, 3).map((person) => person.name ?? person.address ?? "");
|
|
145
|
+
const rest = Math.max(0, (slot.total ?? slot.people.length) - shown.length);
|
|
146
|
+
return { kind: "person", names: shown, rest, more: rest > 0 ? actionMessages(locale).moreCount({ count: rest }) : "", locale };
|
|
147
|
+
}
|
|
148
|
+
case "time":
|
|
149
|
+
return { kind: "time", label, dateTime: slot.start };
|
|
150
|
+
default:
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const partsText = (parts: CapabilitySentencePart[], locale: string, app: ActionCardContext["app"]): ActionText =>
|
|
156
|
+
merge(
|
|
157
|
+
parts.map((part) => {
|
|
158
|
+
if (part.kind === "text") return textPart(part.text);
|
|
159
|
+
// The full label: the chip cuts it for the eye and keeps it whole as its name and tooltip.
|
|
160
|
+
const chip = chipOf(part.slot, part.full, locale, app);
|
|
161
|
+
return chip ? { kind: "chip" as const, chip } : textPart(part.label);
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
/** The plain words of a sentence, as a screen reader or a message reads them. */
|
|
166
|
+
export const actionPlainText = (text: ActionText): string =>
|
|
167
|
+
text
|
|
168
|
+
.map((part) => {
|
|
169
|
+
if (part.kind === "text") return part.text;
|
|
170
|
+
const chip = part.chip;
|
|
171
|
+
if (chip.kind === "person")
|
|
172
|
+
return new Intl.ListFormat(chip.locale, { type: "conjunction" }).format(chip.rest > 0 ? [...chip.names, chip.more] : chip.names);
|
|
173
|
+
return chip.kind === "time" ? chip.label : chip.title;
|
|
174
|
+
})
|
|
175
|
+
.join("");
|
|
176
|
+
|
|
177
|
+
const firstLink = (links: readonly CapabilitySemanticLink[] | undefined, rels: CapabilitySemanticLink["rel"][]) =>
|
|
178
|
+
rels.map((rel) => links?.find((link) => link.rel === rel)?.href).find(Boolean);
|
|
179
|
+
|
|
180
|
+
/** Every link but the one "Read it all" follows, named by its own title or by what it does in the app. */
|
|
181
|
+
const otherLinks = (links: readonly CapabilitySemanticLink[] | undefined, locale: string, app: string) => {
|
|
182
|
+
const readMore = firstLink(links, ["open", "preview"]);
|
|
183
|
+
const t = aiChatMessages(locale);
|
|
184
|
+
const label = (link: CapabilitySemanticLink) =>
|
|
185
|
+
link.title ??
|
|
186
|
+
{ open: t.openIn({ app }), edit: t.editIn({ app }), status: t.statusIn({ app }), preview: t.preview, download: t.download }[link.rel];
|
|
187
|
+
return (links ?? []).filter((link) => link.href !== readMore).map((link) => ({ href: link.href, label: label(link) }));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const firstObjectHref = (review: CapabilityActionCallReview): string | undefined =>
|
|
191
|
+
Object.values(review.slots)
|
|
192
|
+
.map((slot) => ((slot.kind === "object" || slot.kind === "place") && slot.ref ? cloudResourceHref(slot.ref) : undefined))
|
|
193
|
+
.find(Boolean);
|
|
194
|
+
|
|
195
|
+
export const reviewActionCard = (review: CapabilityActionCallReview, context: ActionCardContext): ActionCard => {
|
|
196
|
+
const locale = review.locale;
|
|
197
|
+
const tz = { timeZone: context.timeZone };
|
|
198
|
+
const parts = (form: Parameters<typeof capabilityReviewSentenceParts>[1]) =>
|
|
199
|
+
partsText(capabilityReviewSentenceParts(review, form, tz), locale, context.app);
|
|
200
|
+
const sentence = (text: string, slots: CapabilityActionCallReview["slots"] | undefined) =>
|
|
201
|
+
partsText(
|
|
202
|
+
// A consequence or rule sentence uses its own slots, else the review's.
|
|
203
|
+
capabilityReceiptParts({ text, slots: slots ?? review.slots }, { locale, timeZone: context.timeZone }),
|
|
204
|
+
locale,
|
|
205
|
+
context.app,
|
|
206
|
+
);
|
|
207
|
+
const consequence = (entry: CapabilityConsequence) => ({
|
|
208
|
+
text: sentence(entry.text, entry.slots),
|
|
209
|
+
...(entry.mark ? { mark: entry.mark } : {}),
|
|
210
|
+
});
|
|
211
|
+
const code = review.ask ?? "app_asks";
|
|
212
|
+
const subjectSlot = Object.entries(review.slots).find(([, slot]) => slot.kind === "object" || slot.kind === "place");
|
|
213
|
+
const preview = review.preview;
|
|
214
|
+
return {
|
|
215
|
+
locale,
|
|
216
|
+
app: context.app,
|
|
217
|
+
question: parts("question"),
|
|
218
|
+
questionText: capabilityReviewSentence(review, "question", tz),
|
|
219
|
+
reason: actionReason(code, { locale, appName: context.app.name }),
|
|
220
|
+
preview:
|
|
221
|
+
preview || review.consequences?.length
|
|
222
|
+
? {
|
|
223
|
+
kind: "review",
|
|
224
|
+
locale,
|
|
225
|
+
timeZone: context.timeZone,
|
|
226
|
+
...(preview ? { preview } : {}),
|
|
227
|
+
consequences: (review.consequences ?? []).map(consequence),
|
|
228
|
+
...(preview?.kind === "rule"
|
|
229
|
+
? { when: sentence(preview.when.text, preview.when.slots), may: preview.may.map(consequence) }
|
|
230
|
+
: {}),
|
|
231
|
+
}
|
|
232
|
+
: null,
|
|
233
|
+
readMore: firstLink(review.links, ["open", "preview"]),
|
|
234
|
+
links: otherLinks(review.links, locale, context.app.name),
|
|
235
|
+
open: firstLink(review.links, ["open", "edit", "status"]) ?? firstObjectHref(review),
|
|
236
|
+
confirm: {
|
|
237
|
+
label: capabilityReviewSentence(review, "confirm", tz),
|
|
238
|
+
variant: code === "deletes" || code === "cannot_undo" ? "danger" : "ai",
|
|
239
|
+
},
|
|
240
|
+
decline: capabilityReviewSentence(review, "decline", tz),
|
|
241
|
+
forms: {
|
|
242
|
+
running: parts("running"),
|
|
243
|
+
done: parts("done"),
|
|
244
|
+
rejected: parts("rejected"),
|
|
245
|
+
notRun: parts("notRun"),
|
|
246
|
+
unknown: parts("unknown"),
|
|
247
|
+
unknownStill: parts("unknownStill"),
|
|
248
|
+
},
|
|
249
|
+
subject: subjectSlot
|
|
250
|
+
? partsText(
|
|
251
|
+
capabilityReviewSentenceParts({ ...review, sentence: { ...review.sentence, done: `{${subjectSlot[0]}}` } }, "done", tz),
|
|
252
|
+
locale,
|
|
253
|
+
context.app,
|
|
254
|
+
)
|
|
255
|
+
: plainText(context.app.name),
|
|
256
|
+
short: capabilityReviewSentence(review, "short", tz),
|
|
257
|
+
hasCheck: Boolean(review.check),
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* An old review, or a call without one, as literal text: the question is the old message (or Cloud's sentence for the
|
|
263
|
+
* call), its details are rows. Its outcome lines use Cloud's frames around that sentence.
|
|
264
|
+
*/
|
|
265
|
+
export type LegacyCardInput = {
|
|
266
|
+
question: string;
|
|
267
|
+
rows: AiLegacyReviewRow[];
|
|
268
|
+
markdown?: string;
|
|
269
|
+
links?: readonly CapabilitySemanticLink[];
|
|
270
|
+
summary?: string;
|
|
271
|
+
/** The subject of the outcome lines, such as the app's sentence for the call. */
|
|
272
|
+
subject: string;
|
|
273
|
+
/** The answer's words: the Action's title. */
|
|
274
|
+
title: string;
|
|
275
|
+
outcome: { running: string; done: string; rejected: string; notRun: string; unknown: string; unknownStill: string };
|
|
276
|
+
/** The call's own input, shown on demand when the question has nothing else to show. */
|
|
277
|
+
input?: unknown;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const legacyActionCard = (input: LegacyCardInput, context: ActionCardContext): ActionCard => {
|
|
281
|
+
const t = actionMessages(context.locale);
|
|
282
|
+
const showInput = input.rows.length === 0 && !input.markdown && input.input !== undefined;
|
|
283
|
+
return {
|
|
284
|
+
locale: context.locale,
|
|
285
|
+
app: context.app,
|
|
286
|
+
question: plainText(input.question),
|
|
287
|
+
questionText: input.question,
|
|
288
|
+
reason: actionReason("app_asks", { locale: context.locale, appName: context.app.name }),
|
|
289
|
+
preview:
|
|
290
|
+
input.rows.length > 0 || input.markdown || showInput
|
|
291
|
+
? {
|
|
292
|
+
kind: "legacy",
|
|
293
|
+
rows: input.rows,
|
|
294
|
+
...(input.markdown ? { markdown: input.markdown } : {}),
|
|
295
|
+
...(showInput ? { input: input.input } : {}),
|
|
296
|
+
}
|
|
297
|
+
: null,
|
|
298
|
+
readMore: firstLink(input.links, ["open", "preview"]),
|
|
299
|
+
links: otherLinks(input.links, context.locale, context.app.name),
|
|
300
|
+
open: firstLink(input.links, ["open", "edit", "status"]),
|
|
301
|
+
confirm: { label: input.title, variant: "ai" },
|
|
302
|
+
decline: t.decline,
|
|
303
|
+
forms: {
|
|
304
|
+
running: plainText(input.outcome.running),
|
|
305
|
+
done: plainText(input.summary ?? input.outcome.done),
|
|
306
|
+
rejected: plainText(input.outcome.rejected),
|
|
307
|
+
notRun: plainText(input.outcome.notRun),
|
|
308
|
+
unknown: plainText(input.outcome.unknown),
|
|
309
|
+
unknownStill: plainText(input.outcome.unknownStill),
|
|
310
|
+
},
|
|
311
|
+
subject: plainText(input.subject),
|
|
312
|
+
short: input.subject,
|
|
313
|
+
hasCheck: false,
|
|
314
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** The receipt an Action returned, or the result of its `check`, as a sentence in the review's language. */
|
|
319
|
+
export const actionReceiptText = (receipt: CapabilityActionReceipt, card: ActionCard, timeZone: string): ActionText =>
|
|
320
|
+
partsText(capabilityReceiptParts(receipt, { locale: card.locale, timeZone }), card.locale, card.app);
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Tool blocks
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/** The review of a call: the one Cloud stored, else the one its open approval carries. */
|
|
327
|
+
export const toolBlockReview = (block: ToolBlock): AiStoredActionReview | null =>
|
|
328
|
+
block.review ?? (block.approval?.review ? storedActionReview(block.approval.review) : null);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The lines an approval message adds for a Cloud tool, such as "HTTP request: GET https://…": the first line naming
|
|
332
|
+
* the app and the call repeats the question, and the generic hint says nothing.
|
|
333
|
+
*/
|
|
334
|
+
const messageRows = (block: ToolBlock): AiLegacyReviewRow[] => {
|
|
335
|
+
const message = block.approval?.message?.trim();
|
|
336
|
+
if (!message || block.presentation?.kind === "capability") return [];
|
|
337
|
+
const rows: AiLegacyReviewRow[] = [];
|
|
338
|
+
// "Label: value" is one row; "Label:" alone starts a row whose value is the lines after it.
|
|
339
|
+
let open: AiLegacyReviewRow | null = null;
|
|
340
|
+
for (const line of message.split("\n").map((entry) => entry.trim())) {
|
|
341
|
+
if (!line || /^Review the validated arguments below before running this action\.$/i.test(line)) continue;
|
|
342
|
+
const heading = /^([^:]{1,40}):$/.exec(line);
|
|
343
|
+
const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
|
|
344
|
+
if (heading) {
|
|
345
|
+
open = { label: heading[1]!, value: "" };
|
|
346
|
+
rows.push(open);
|
|
347
|
+
} else if (match) {
|
|
348
|
+
open = null;
|
|
349
|
+
rows.push({ label: match[1]!, value: match[2]! });
|
|
350
|
+
} else if (open) open.value = open.value ? `${open.value}\n${line}` : line;
|
|
351
|
+
else rows.push({ label: "", value: line });
|
|
352
|
+
}
|
|
353
|
+
return rows;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export type ToolOutcomeWords = LegacyCardInput["outcome"];
|
|
357
|
+
|
|
358
|
+
/** The card for one tool block, in the reader's locale unless its review names another. */
|
|
359
|
+
export const toolActionCard = (
|
|
360
|
+
block: ToolBlock,
|
|
361
|
+
context: { locale: string; timeZone: string; outcome: (subject: string) => ToolOutcomeWords },
|
|
362
|
+
): ActionCard => {
|
|
363
|
+
const presentation = block.presentation;
|
|
364
|
+
const app = {
|
|
365
|
+
name: presentation?.appName ?? "Cloud",
|
|
366
|
+
icon: aiToolIcon(block.name, presentation?.appIcon),
|
|
367
|
+
...(presentation?.appAccent ? { accent: presentation.appAccent } : {}),
|
|
368
|
+
};
|
|
369
|
+
const cardContext = { locale: context.locale, timeZone: context.timeZone, app };
|
|
370
|
+
const review = toolBlockReview(block);
|
|
371
|
+
if (review && !isLegacyActionReview(review)) return reviewActionCard(review, cardContext);
|
|
372
|
+
const subject =
|
|
373
|
+
toolSubject(block, context.locale, context.timeZone) ?? presentation?.title ?? displayToolName(block.name, context.locale);
|
|
374
|
+
const title = presentation?.title ?? subject;
|
|
375
|
+
const legacy = review?.legacy;
|
|
376
|
+
return legacyActionCard(
|
|
377
|
+
{
|
|
378
|
+
// An old review's message stays literal; Cloud's own sentence for the call is asked as a question.
|
|
379
|
+
question: legacy?.question ?? capabilityQuestionText(subject, context.locale),
|
|
380
|
+
rows: legacy?.rows ?? messageRows(block),
|
|
381
|
+
...(legacy?.markdown ? { markdown: legacy.markdown } : {}),
|
|
382
|
+
...(legacy?.links ? { links: legacy.links } : {}),
|
|
383
|
+
...(legacy?.summary ? { summary: legacy.summary } : {}),
|
|
384
|
+
subject,
|
|
385
|
+
title,
|
|
386
|
+
outcome: context.outcome(subject),
|
|
387
|
+
input: block.args,
|
|
388
|
+
},
|
|
389
|
+
cardContext,
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/** An old review read from a pending approval, the same way the chat stores it. */
|
|
394
|
+
export const legacyReviewOf = (review: CapabilityActionReview) => legacyActionReview(review).legacy;
|
|
395
|
+
|
|
396
|
+
/** The action sentence of a review as plain text, without the question mark. */
|
|
397
|
+
export const reviewActionText = (review: CapabilityActionCallReview, timeZone: string): string =>
|
|
398
|
+
capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone });
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cloud's own words around an Action question and its outcome line: details under the line, the preview's labels, and
|
|
5
|
+
* the answers of a question that has no app sentence yet. The question, its chips, and the buttons of a new review
|
|
6
|
+
* are the app's words; reasons are Cloud's reason codes (`capability-sentences.ts`).
|
|
7
|
+
*/
|
|
8
|
+
const messages = i18n.define({
|
|
9
|
+
baseLocale: "en",
|
|
10
|
+
messages: {
|
|
11
|
+
en: {
|
|
12
|
+
declined: "You declined.",
|
|
13
|
+
declinedElsewhere: "You declined in another window.",
|
|
14
|
+
approvedElsewhere: "You approved in another window.",
|
|
15
|
+
expired: "The question expired.",
|
|
16
|
+
stopped: "The response was stopped.",
|
|
17
|
+
askAgain: "Ask again",
|
|
18
|
+
askAgainMessage: ({ sentence }: { sentence: string }) => `Please ask me again: ${sentence}`,
|
|
19
|
+
lost: "Your answer didn't arrive.",
|
|
20
|
+
lostAbout: ({ subject }: { subject: string }) => `Your answer about ${subject} didn't arrive.`,
|
|
21
|
+
lostDetail: "If it did arrive after all, its outcome shows here.",
|
|
22
|
+
answerAgain: "Answer again",
|
|
23
|
+
checking: ({ app }: { app: string }) => `The connection dropped. I'm asking ${app} for the status …`,
|
|
24
|
+
checkedDone: ({ app }: { app: string }) => `${app} already did it. I won't run it again.`,
|
|
25
|
+
checkedNotDone: ({ app }: { app: string }) => `${app} didn't do it.`,
|
|
26
|
+
checkPending: ({ app }: { app: string }) => `${app} doesn't know yet either. Running it again waits until that is clear.`,
|
|
27
|
+
checkFailed: ({ app }: { app: string }) => `${app} isn't answering right now. Running it again waits until that is clear.`,
|
|
28
|
+
noCheck: "The connection dropped. Running it again waits until it is clear what happened.",
|
|
29
|
+
cutOff: "The response ended while this ran. Running it again waits until it is clear what happened.",
|
|
30
|
+
checkAgain: "Check again",
|
|
31
|
+
checkAgainFailed: "The check didn't go through. Try again later.",
|
|
32
|
+
openIn: ({ app }: { app: string }) => `Open in ${app}`,
|
|
33
|
+
decline: "Don't run",
|
|
34
|
+
remember: "Yes, and don't ask again in this chat",
|
|
35
|
+
rememberWebsite: "Yes, and allow this website in this chat",
|
|
36
|
+
unavailable: "You can't answer this question here.",
|
|
37
|
+
stopping: "The response is stopping.",
|
|
38
|
+
submitFailed: "Your answer didn't go through. Try again.",
|
|
39
|
+
// Preview
|
|
40
|
+
from: "From",
|
|
41
|
+
to: "To",
|
|
42
|
+
cc: "Cc",
|
|
43
|
+
bcc: "Bcc",
|
|
44
|
+
andMore: ({ count }: { count: number }) => `and ${count.toLocaleString("en")} more`,
|
|
45
|
+
/** The last item of a list that the list format joins with "and". */
|
|
46
|
+
moreCount: ({ count }: { count: number }) => `${count.toLocaleString("en")} more`,
|
|
47
|
+
sendsNow: "No Cc or Bcc. Sends right away.",
|
|
48
|
+
sendsNowWithCopies: "Sends right away.",
|
|
49
|
+
sendsAt: ({ at }: { at: string }) => `Sends on ${at}.`,
|
|
50
|
+
noSubject: "(no subject)",
|
|
51
|
+
readMail: "Read the whole email",
|
|
52
|
+
readPage: "Read it all",
|
|
53
|
+
readRule: "Read the whole instruction",
|
|
54
|
+
readMore: "Read it all",
|
|
55
|
+
readMoreTitle: "Opens exactly this version",
|
|
56
|
+
moreAttachments: ({ count }: { count: number }) => `${count.toLocaleString("en")} more ${count === 1 ? "attachment" : "attachments"}`,
|
|
57
|
+
allDay: "all day",
|
|
58
|
+
due: ({ at }: { at: string }) => `Due ${at}`,
|
|
59
|
+
with: "With",
|
|
60
|
+
ruleMay: "What it may do",
|
|
61
|
+
ruleBlindToChat: "Doesn't know this chat. It only knows what is written above.",
|
|
62
|
+
changes: "What changes",
|
|
63
|
+
moveTo: ({ to }: { to: string }) => `To ${to}`,
|
|
64
|
+
before: "before",
|
|
65
|
+
showInput: "Show input",
|
|
66
|
+
hideInput: "Hide input",
|
|
67
|
+
input: "Input",
|
|
68
|
+
},
|
|
69
|
+
de: {
|
|
70
|
+
declined: "Du hast abgelehnt.",
|
|
71
|
+
declinedElsewhere: "Du hast in einem anderen Fenster abgelehnt.",
|
|
72
|
+
approvedElsewhere: "Du hast in einem anderen Fenster freigegeben.",
|
|
73
|
+
expired: "Die Frage ist abgelaufen.",
|
|
74
|
+
stopped: "Die Antwort wurde angehalten.",
|
|
75
|
+
askAgain: "Noch einmal fragen",
|
|
76
|
+
askAgainMessage: ({ sentence }) => `Frag mich bitte noch einmal: ${sentence}`,
|
|
77
|
+
lost: "Deine Antwort kam nicht an.",
|
|
78
|
+
lostAbout: ({ subject }) => `Deine Antwort zu ${subject} kam nicht an.`,
|
|
79
|
+
lostDetail: "Kam sie doch an, siehst du das Ergebnis gleich hier.",
|
|
80
|
+
answerAgain: "Noch einmal antworten",
|
|
81
|
+
checking: ({ app }) => `Die Verbindung brach ab. Ich frage ${app} nach dem Stand …`,
|
|
82
|
+
checkedDone: ({ app }) => `${app} hat es schon erledigt. Ich führe es nicht noch einmal aus.`,
|
|
83
|
+
checkedNotDone: ({ app }) => `${app} hat es nicht ausgeführt.`,
|
|
84
|
+
checkPending: ({ app }) => `${app} weiß es selbst noch nicht. Erneut ausführen geht erst, wenn das klar ist.`,
|
|
85
|
+
checkFailed: ({ app }) => `${app} antwortet gerade nicht. Erneut ausführen geht erst, wenn das klar ist.`,
|
|
86
|
+
noCheck: "Die Verbindung brach ab. Erneut ausführen geht erst, wenn klar ist, was passiert ist.",
|
|
87
|
+
cutOff: "Die Antwort endete, während das lief. Erneut ausführen geht erst, wenn klar ist, was passiert ist.",
|
|
88
|
+
checkAgain: "Noch einmal prüfen",
|
|
89
|
+
checkAgainFailed: "Das Prüfen ging nicht durch. Versuche es später noch einmal.",
|
|
90
|
+
openIn: ({ app }) => `In ${app} öffnen`,
|
|
91
|
+
decline: "Nicht ausführen",
|
|
92
|
+
remember: "Ja, und in diesem Chat nicht mehr fragen",
|
|
93
|
+
rememberWebsite: "Ja, und diese Website in diesem Chat erlauben",
|
|
94
|
+
unavailable: "Diese Frage kannst du hier nicht beantworten.",
|
|
95
|
+
stopping: "Die Antwort wird angehalten.",
|
|
96
|
+
submitFailed: "Deine Antwort ging nicht durch. Versuche es noch einmal.",
|
|
97
|
+
from: "Von",
|
|
98
|
+
to: "An",
|
|
99
|
+
cc: "Cc",
|
|
100
|
+
bcc: "Bcc",
|
|
101
|
+
andMore: ({ count }) => `und ${count.toLocaleString("de")} weitere`,
|
|
102
|
+
moreCount: ({ count }) => `${count.toLocaleString("de")} weitere`,
|
|
103
|
+
sendsNow: "Kein Cc oder Bcc. Wird sofort gesendet.",
|
|
104
|
+
sendsNowWithCopies: "Wird sofort gesendet.",
|
|
105
|
+
sendsAt: ({ at }) => `Wird am ${at} gesendet.`,
|
|
106
|
+
noSubject: "(kein Betreff)",
|
|
107
|
+
readMail: "Ganze Mail lesen",
|
|
108
|
+
readPage: "Ganz lesen",
|
|
109
|
+
readRule: "Ganze Anleitung lesen",
|
|
110
|
+
readMore: "Ganz lesen",
|
|
111
|
+
readMoreTitle: "Öffnet genau diese Fassung",
|
|
112
|
+
moreAttachments: ({ count }) => `${count.toLocaleString("de")} ${count === 1 ? "weiterer Anhang" : "weitere Anhänge"}`,
|
|
113
|
+
allDay: "ganztägig",
|
|
114
|
+
due: ({ at }) => `Fällig am ${at}`,
|
|
115
|
+
with: "Mit",
|
|
116
|
+
ruleMay: "Was sie darf",
|
|
117
|
+
ruleBlindToChat: "Kennt diesen Chat nicht. Sie weiß nur, was oben steht.",
|
|
118
|
+
changes: "Was sich ändert",
|
|
119
|
+
moveTo: ({ to }) => `Nach ${to}`,
|
|
120
|
+
before: "vorher",
|
|
121
|
+
showInput: "Eingaben anzeigen",
|
|
122
|
+
hideInput: "Eingaben ausblenden",
|
|
123
|
+
input: "Eingaben",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const actionMessages = (locale: string) => messages.resolve([locale]).t;
|
|
129
|
+
export const checkActionMessages = () => messages.check();
|