@k2b/cloud 0.39.0 → 0.41.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 +11 -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 +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -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 +464 -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 +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -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 +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- 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 +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -4
- 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-actions.ts +31 -7
- 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/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- 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 +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- 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 +166 -262
- package/src/approvals/approval-card.ts +68 -0
- 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 +123 -16
- 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/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -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/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- 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 +13 -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/pdf/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- 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/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -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/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- 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 +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -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/approvals.css +6 -0
- package/src/styles/effects.css +213 -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
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { dates } from "@k2b/stdlib";
|
|
2
|
-
import {
|
|
3
|
-
Button,
|
|
4
|
-
ButtonLink,
|
|
5
|
-
type DropdownItem,
|
|
6
|
-
dialogCore,
|
|
7
|
-
MarkdownView,
|
|
8
|
-
PanelDialog,
|
|
9
|
-
Placeholder,
|
|
10
|
-
panelDialogOptions,
|
|
11
|
-
prompts,
|
|
12
|
-
SplitButton,
|
|
13
|
-
useLocale,
|
|
14
|
-
} from "@k2b/ui";
|
|
2
|
+
import { Button, dialogCore, PanelDialog, Placeholder, panelDialogOptions, prompts, useLocale } from "@k2b/ui";
|
|
15
3
|
import { createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
|
|
4
|
+
import type { ActionCard } from "../ai/chat/action-card";
|
|
5
|
+
import { actionMessages } from "../ai/chat/action-messages";
|
|
6
|
+
import { ActionQuestion, ActionReceipt } from "../ai/chat/action-question";
|
|
16
7
|
import { refreshAppBadges } from "../browser/app-badges";
|
|
17
8
|
import { coreClient } from "../clients/core";
|
|
18
9
|
import {
|
|
@@ -23,13 +14,11 @@ import {
|
|
|
23
14
|
ApprovalPageSchema,
|
|
24
15
|
type PendingApproval,
|
|
25
16
|
} from "../contracts/approvals";
|
|
26
|
-
import
|
|
17
|
+
import { approvalCard } from "./approval-card";
|
|
27
18
|
import { approvalMessages } from "./messages";
|
|
28
19
|
|
|
29
20
|
/** How a decided request ends in this list. It stays in place as a receipt until the next load. */
|
|
30
21
|
type Outcome = "approved" | "rejected" | "elsewhere" | "gone";
|
|
31
|
-
/** Approved and rejected are this person's decision; the other two mean the click changed nothing. */
|
|
32
|
-
const decidedHere = (outcome: Outcome) => outcome === "approved" || outcome === "rejected";
|
|
33
22
|
/** One icon per outcome, so a rejection or a decision made elsewhere never looks like a success. */
|
|
34
23
|
const outcomeIcon = (outcome: Outcome, band = false) =>
|
|
35
24
|
outcome === "approved"
|
|
@@ -41,8 +30,9 @@ const outcomeIcon = (outcome: Outcome, band = false) =>
|
|
|
41
30
|
? "ti ti-circle-x"
|
|
42
31
|
: "ti ti-x"
|
|
43
32
|
: "ti ti-info-circle";
|
|
44
|
-
type ReviewDetail = NonNullable<CapabilityActionReview["details"]>[number];
|
|
45
33
|
|
|
34
|
+
/** Keep SSR and the first client render identical: the page has no reader zone of its own yet. */
|
|
35
|
+
const READER_TIME_ZONE = "UTC";
|
|
46
36
|
export type ApprovalsProps = {
|
|
47
37
|
/** List the requests whose effect belongs to this app or that this app asked for. Without, every app's, grouped. */
|
|
48
38
|
app?: string;
|
|
@@ -51,8 +41,8 @@ export type ApprovalsProps = {
|
|
|
51
41
|
/** The first page for the same query, loaded during SSR with `loadPendingApprovals`; without, the browser loads it. */
|
|
52
42
|
initial?: ApprovalPage;
|
|
53
43
|
/**
|
|
54
|
-
* `list` shows the
|
|
55
|
-
* row above the resource it would change, and nothing when none waits.
|
|
44
|
+
* `list` shows each request as the same question card as the chat, with a count and an empty state. `band` shows
|
|
45
|
+
* each request as one compact row above the resource it would change, and nothing when none waits.
|
|
56
46
|
*/
|
|
57
47
|
layout?: "list" | "band";
|
|
58
48
|
};
|
|
@@ -65,80 +55,15 @@ const useMessages = () => {
|
|
|
65
55
|
return () => approvalMessages.resolve([locale()]).t;
|
|
66
56
|
};
|
|
67
57
|
|
|
68
|
-
const
|
|
69
|
-
item.sentence
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const approvalTitle = (item: PendingApproval): string => {
|
|
76
|
-
const first = lines(item)[0] ?? item.action;
|
|
77
|
-
const prefix = `${item.app.name}: `;
|
|
78
|
-
return first.startsWith(prefix) && first.length > prefix.length ? first.slice(prefix.length) : first;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/** Without a review, the sentence's later `Label: value` lines are what the person approves. */
|
|
82
|
-
const sentenceFields = (item: PendingApproval) =>
|
|
83
|
-
lines(item)
|
|
84
|
-
.slice(1)
|
|
85
|
-
.map((line) => {
|
|
86
|
-
const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
|
|
87
|
-
return match ? { label: match[1], value: match[2] ?? "" } : { label: null, value: line };
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const detailText = (detail: ReviewDetail, locale: string): string => {
|
|
91
|
-
if (detail.format === "date")
|
|
92
|
-
return /^\d{4}-\d{2}-\d{2}$/.test(detail.value)
|
|
93
|
-
? dates.formatDate(`${detail.value}T12:00:00.000Z`, { locale, timeZone: "UTC" })
|
|
94
|
-
: dates.formatDate(detail.value, { locale });
|
|
95
|
-
if (detail.format === "date-time") return dates.formatDateTime(detail.value, { locale });
|
|
96
|
-
return detail.value;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const inlineDetails = (item: PendingApproval) => (item.review?.details ?? []).filter((detail) => (detail.display ?? "inline") === "inline");
|
|
100
|
-
const blockDetails = (item: PendingApproval) => (item.review?.details ?? []).filter((detail) => detail.display === "block");
|
|
101
|
-
|
|
102
|
-
function Fields(props: { item: PendingApproval }) {
|
|
103
|
-
const locale = useLocale();
|
|
104
|
-
const fields = () =>
|
|
105
|
-
props.item.review
|
|
106
|
-
? inlineDetails(props.item).map((detail) => ({ label: detail.label, value: detailText(detail, locale()) }))
|
|
107
|
-
: sentenceFields(props.item).filter((field): field is { label: string; value: string } => field.label !== null);
|
|
108
|
-
return (
|
|
109
|
-
<Show when={fields().length > 0}>
|
|
110
|
-
<dl class="cloud-approval__fields">
|
|
111
|
-
<For each={fields()}>
|
|
112
|
-
{(field) => (
|
|
113
|
-
<>
|
|
114
|
-
<dt>{field.label}</dt>
|
|
115
|
-
<dd>{field.value}</dd>
|
|
116
|
-
</>
|
|
117
|
-
)}
|
|
118
|
-
</For>
|
|
119
|
-
</dl>
|
|
120
|
-
</Show>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** The review's message, or the sentence's plain lines when the Action has no review. */
|
|
125
|
-
function Description(props: { item: PendingApproval }) {
|
|
126
|
-
const text = () => {
|
|
127
|
-
const message = props.item.review?.message.trim();
|
|
128
|
-
if (message) return message === approvalTitle(props.item) ? null : message;
|
|
129
|
-
const plain = sentenceFields(props.item)
|
|
130
|
-
.filter((field) => field.label === null)
|
|
131
|
-
.map((field) => field.value);
|
|
132
|
-
return plain.length ? plain.join("\n") : null;
|
|
133
|
-
};
|
|
134
|
-
return (
|
|
135
|
-
<Show when={text()}>
|
|
136
|
-
<p class="cloud-approval__text">{text()}</p>
|
|
137
|
-
</Show>
|
|
138
|
-
);
|
|
139
|
-
}
|
|
58
|
+
const cardInput = (item: PendingApproval) => ({
|
|
59
|
+
sentence: item.sentence,
|
|
60
|
+
action: item.action,
|
|
61
|
+
appName: item.app.name,
|
|
62
|
+
appIcon: item.app.icon,
|
|
63
|
+
review: item.review,
|
|
64
|
+
});
|
|
140
65
|
|
|
141
|
-
/** Origin of the request: owning app, the chat it waits in,
|
|
66
|
+
/** Origin of the request: owning app, the chat it waits in, when Cloud asked, and when it expires. */
|
|
142
67
|
function Origin(props: { item: PendingApproval; withApp?: boolean }) {
|
|
143
68
|
const t = useMessages();
|
|
144
69
|
const locale = useLocale();
|
|
@@ -153,168 +78,131 @@ function Origin(props: { item: PendingApproval; withApp?: boolean }) {
|
|
|
153
78
|
</a>
|
|
154
79
|
<span aria-hidden="true"> · </span>
|
|
155
80
|
<time dateTime={props.item.createdAt}>{dates.formatDateTimeRelative(props.item.createdAt, { locale: locale() })}</time>
|
|
81
|
+
<span aria-hidden="true"> · </span>
|
|
82
|
+
<span>{t().expiresIn({ time: dates.formatTimeSpan(props.item.expiresAt, { locale: locale() }) })}</span>
|
|
156
83
|
</p>
|
|
157
84
|
);
|
|
158
85
|
}
|
|
159
86
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const text
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
role={props.failed ? "alert" : undefined}
|
|
185
|
-
>
|
|
186
|
-
<i class={icon()} aria-hidden="true" />
|
|
187
|
-
<span title={text()}>{text()}</span>
|
|
188
|
-
</span>
|
|
189
|
-
);
|
|
87
|
+
/** The outcome lines a request on this page can reach; the chat it came from shows how the Action itself ends. */
|
|
88
|
+
const outcomeLines = (card: ActionCard, words: ReturnType<typeof approvalMessages.resolve>["t"], reopen: () => void) => ({
|
|
89
|
+
sending: { tone: "running" as const, text: [{ kind: "text" as const, text: words.sending }] },
|
|
90
|
+
approved: { tone: "done" as const, text: [{ kind: "text" as const, text: words.approvedHere }], detail: words.continuesInChat },
|
|
91
|
+
rejected: { tone: "off" as const, text: card.forms.rejected, detail: words.declinedHere },
|
|
92
|
+
elsewhere: { tone: "off" as const, icon: "ti ti-info-circle", text: [{ kind: "text" as const, text: words.decidedElsewhere }] },
|
|
93
|
+
gone: { tone: "off" as const, icon: "ti ti-clock-x", text: [{ kind: "text" as const, text: words.gone }] },
|
|
94
|
+
failed: {
|
|
95
|
+
tone: "warning" as const,
|
|
96
|
+
icon: "ti ti-alert-triangle",
|
|
97
|
+
text: [{ kind: "text" as const, text: words.notApplied }],
|
|
98
|
+
detail: words.decisionFailed,
|
|
99
|
+
action: { label: words.answerAgain, onSelect: reopen },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** Answers that arm late, so a click meant for what was there before cannot approve a request that just appeared. */
|
|
104
|
+
function Armed(props: { children: JSX.Element }) {
|
|
105
|
+
const [armed, setArmed] = createSignal(false);
|
|
106
|
+
onMount(() => {
|
|
107
|
+
const timer = setTimeout(() => setArmed(true), ARM_DELAY_MS);
|
|
108
|
+
onCleanup(() => clearTimeout(timer));
|
|
109
|
+
});
|
|
110
|
+
return <div inert={armed() ? undefined : true}>{props.children}</div>;
|
|
190
111
|
}
|
|
191
112
|
|
|
192
|
-
|
|
113
|
+
/** One request as the chat's question card. Its answers arm late, so a click meant for the page cannot approve it. */
|
|
114
|
+
function ApprovalCard(props: {
|
|
115
|
+
item: PendingApproval;
|
|
116
|
+
outcome?: Outcome;
|
|
117
|
+
busy: boolean;
|
|
118
|
+
failed: boolean;
|
|
119
|
+
decide: (input: ApprovalDecisionInput) => void;
|
|
120
|
+
reopen: () => void;
|
|
121
|
+
}) {
|
|
193
122
|
const t = useMessages();
|
|
194
|
-
const
|
|
123
|
+
const locale = useLocale();
|
|
124
|
+
const card = createMemo(() => approvalCard(cardInput(props.item), locale(), READER_TIME_ZONE));
|
|
125
|
+
const current = () => props.outcome ?? (props.failed ? "failed" : "sending");
|
|
126
|
+
const state = () => (props.outcome || props.busy || props.failed ? "decided" : "open");
|
|
127
|
+
// `confirm` asks every time, whatever the stored options say.
|
|
128
|
+
const remember = () =>
|
|
129
|
+
props.item.remember.chat && props.item.assurance === "standard"
|
|
130
|
+
? {
|
|
131
|
+
label: props.item.remember.website ? actionMessages(card().locale).rememberWebsite : actionMessages(card().locale).remember,
|
|
132
|
+
onSelect: () => props.decide({ approved: true, remember: "chat" }),
|
|
133
|
+
}
|
|
134
|
+
: undefined;
|
|
195
135
|
return (
|
|
196
|
-
<
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
136
|
+
<article class="cloud-approval" data-decided={props.outcome} aria-label={t().approvalFor({ title: card().questionText })}>
|
|
137
|
+
<Origin item={props.item} />
|
|
138
|
+
<Armed>
|
|
139
|
+
<ActionQuestion
|
|
140
|
+
card={card()}
|
|
141
|
+
state={state()}
|
|
142
|
+
timeZone={READER_TIME_ZONE}
|
|
143
|
+
remember={remember()}
|
|
144
|
+
onConfirm={() => props.decide({ approved: true })}
|
|
145
|
+
onDecline={() => props.decide({ approved: false })}
|
|
146
|
+
// The outcome lines join the card's sentence, so they speak the stored review's language.
|
|
147
|
+
outcome={
|
|
148
|
+
<ActionReceipt
|
|
149
|
+
card={card()}
|
|
150
|
+
lines={outcomeLines(card(), approvalMessages.resolve([card().locale]).t, props.reopen)}
|
|
151
|
+
current={current()}
|
|
152
|
+
/>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
</Armed>
|
|
156
|
+
</article>
|
|
200
157
|
);
|
|
201
158
|
}
|
|
202
159
|
|
|
203
|
-
/** Reject, then approve
|
|
204
|
-
function
|
|
160
|
+
/** Reject, then approve, for the compact band; the full card opens in a panel. */
|
|
161
|
+
function BandDecision(props: {
|
|
162
|
+
item: PendingApproval;
|
|
163
|
+
busy: boolean;
|
|
164
|
+
details: () => void;
|
|
165
|
+
decide: (input: ApprovalDecisionInput) => void;
|
|
166
|
+
}) {
|
|
205
167
|
const t = useMessages();
|
|
206
168
|
const [armed, setArmed] = createSignal(false);
|
|
207
169
|
onMount(() => {
|
|
208
170
|
const timer = setTimeout(() => setArmed(true), ARM_DELAY_MS);
|
|
209
171
|
onCleanup(() => clearTimeout(timer));
|
|
210
172
|
});
|
|
211
|
-
const rememberItems = (): DropdownItem[] => [
|
|
212
|
-
...(props.item.remember.chat
|
|
213
|
-
? [
|
|
214
|
-
{
|
|
215
|
-
label: props.item.remember.website ? t().allowWebsiteForChat : t().approveForChat,
|
|
216
|
-
icon: props.item.remember.website ? "ti ti-world-check" : "ti ti-message-check",
|
|
217
|
-
action: () => props.decide({ approved: true, remember: "chat" }),
|
|
218
|
-
},
|
|
219
|
-
]
|
|
220
|
-
: []),
|
|
221
|
-
...(props.item.remember.always
|
|
222
|
-
? [{ label: t().alwaysApprove, icon: "ti ti-shield-check", action: () => props.decide({ approved: true, remember: "always" }) }]
|
|
223
|
-
: []),
|
|
224
|
-
];
|
|
225
173
|
return (
|
|
226
174
|
// Inert, not disabled, while arming: the buttons look the same from the first frame, so nothing flickers.
|
|
227
175
|
<div class="cloud-approval__actions" inert={armed() ? undefined : true}>
|
|
228
|
-
<
|
|
229
|
-
{(
|
|
230
|
-
|
|
231
|
-
{t().details}
|
|
232
|
-
</Button>
|
|
233
|
-
)}
|
|
234
|
-
</Show>
|
|
176
|
+
<Button size="sm" variant="ghost" onClick={() => props.details()}>
|
|
177
|
+
{t().details}
|
|
178
|
+
</Button>
|
|
235
179
|
<Button size="sm" variant="ghost" disabled={props.busy} onClick={() => props.decide({ approved: false })}>
|
|
236
180
|
{t().reject}
|
|
237
181
|
</Button>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
|
|
242
|
-
{t().approve}
|
|
243
|
-
</Button>
|
|
244
|
-
}
|
|
245
|
-
>
|
|
246
|
-
<SplitButton
|
|
247
|
-
size="sm"
|
|
248
|
-
variant="primary"
|
|
249
|
-
loading={props.busy}
|
|
250
|
-
menuLabel={t().moreOptions}
|
|
251
|
-
menuPosition="bottom-right"
|
|
252
|
-
items={rememberItems()}
|
|
253
|
-
onClick={() => props.decide({ approved: true })}
|
|
254
|
-
>
|
|
255
|
-
{t().approve}
|
|
256
|
-
</SplitButton>
|
|
257
|
-
</Show>
|
|
182
|
+
<Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
|
|
183
|
+
{t().approve}
|
|
184
|
+
</Button>
|
|
258
185
|
</div>
|
|
259
186
|
);
|
|
260
187
|
}
|
|
261
188
|
|
|
262
|
-
function
|
|
189
|
+
function Receipt(props: { outcome: Outcome }) {
|
|
263
190
|
const t = useMessages();
|
|
264
|
-
const
|
|
265
|
-
({
|
|
266
|
-
open: t().openIn({ app: props.item.app.name }),
|
|
267
|
-
edit: t().editIn({ app: props.item.app.name }),
|
|
268
|
-
status: t().statusIn({ app: props.item.app.name }),
|
|
269
|
-
preview: t().preview,
|
|
270
|
-
download: t().download,
|
|
271
|
-
})[rel];
|
|
272
|
-
return (
|
|
273
|
-
<Show when={(props.item.review?.links ?? []).length > 0}>
|
|
274
|
-
<nav class="cloud-approval__links" aria-label={t().approvalFor({ title: approvalTitle(props.item) })}>
|
|
275
|
-
<For each={props.item.review?.links ?? []}>
|
|
276
|
-
{(link) => (
|
|
277
|
-
<ButtonLink href={link.href} size="sm" variant="ghost">
|
|
278
|
-
<i class="ti ti-external-link" aria-hidden="true" />
|
|
279
|
-
{link.title ?? title(link.rel)}
|
|
280
|
-
</ButtonLink>
|
|
281
|
-
)}
|
|
282
|
-
</For>
|
|
283
|
-
</nav>
|
|
284
|
-
</Show>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function BlockDetails(props: { item: PendingApproval; clamp?: boolean }) {
|
|
289
|
-
const locale = useLocale();
|
|
191
|
+
const label = () => ({ approved: t().approved, rejected: t().rejected, elsewhere: t().decidedElsewhere, gone: t().gone })[props.outcome];
|
|
290
192
|
return (
|
|
291
|
-
<
|
|
292
|
-
{(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<h4 class="cloud-approval__label">{detail.label}</h4>
|
|
296
|
-
</Show>
|
|
297
|
-
<Show
|
|
298
|
-
when={detail.format === "markdown" && !props.clamp}
|
|
299
|
-
fallback={
|
|
300
|
-
<p class="cloud-approval__quote" data-clamp={props.clamp ? "true" : undefined}>
|
|
301
|
-
<span>{detailText(detail, locale())}</span>
|
|
302
|
-
</p>
|
|
303
|
-
}
|
|
304
|
-
>
|
|
305
|
-
<MarkdownView class="cloud-approval__markdown" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
306
|
-
</Show>
|
|
307
|
-
</section>
|
|
308
|
-
)}
|
|
309
|
-
</For>
|
|
193
|
+
<span class="cloud-approval__receipt" data-outcome={props.outcome}>
|
|
194
|
+
<i class={outcomeIcon(props.outcome)} aria-hidden="true" />
|
|
195
|
+
{label()}
|
|
196
|
+
</span>
|
|
310
197
|
);
|
|
311
198
|
}
|
|
312
199
|
|
|
313
|
-
/** Opens the full request in a panel; resolves with the decision made there, if any. */
|
|
200
|
+
/** Opens the full request in a panel as the chat's card; resolves with the decision made there, if any. */
|
|
314
201
|
const openDetails = (item: PendingApproval, locale: string) =>
|
|
315
202
|
dialogCore.open<ApprovalDecisionInput>(
|
|
316
203
|
(close) => {
|
|
317
204
|
const t = () => approvalMessages.resolve([locale]).t;
|
|
205
|
+
const card = approvalCard(cardInput(item), locale, READER_TIME_ZONE);
|
|
318
206
|
return (
|
|
319
207
|
<PanelDialog>
|
|
320
208
|
<PanelDialog.Header
|
|
@@ -326,29 +214,40 @@ const openDetails = (item: PendingApproval, locale: string) =>
|
|
|
326
214
|
/>
|
|
327
215
|
<PanelDialog.Body>
|
|
328
216
|
<div class="cloud-approval__sheet">
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
217
|
+
<Armed>
|
|
218
|
+
<ActionQuestion
|
|
219
|
+
card={card}
|
|
220
|
+
state="open"
|
|
221
|
+
timeZone={READER_TIME_ZONE}
|
|
222
|
+
remember={
|
|
223
|
+
item.remember.chat && item.assurance === "standard"
|
|
224
|
+
? {
|
|
225
|
+
label: item.remember.website ? actionMessages(card.locale).rememberWebsite : actionMessages(card.locale).remember,
|
|
226
|
+
onSelect: () => close({ approved: true, remember: "chat" }),
|
|
227
|
+
}
|
|
228
|
+
: undefined
|
|
229
|
+
}
|
|
230
|
+
onConfirm={() => close({ approved: true })}
|
|
231
|
+
onDecline={() => close({ approved: false })}
|
|
232
|
+
outcome={null}
|
|
233
|
+
/>
|
|
234
|
+
</Armed>
|
|
334
235
|
<p class="cloud-approval__callout">
|
|
335
236
|
<i class="ti ti-info-circle" aria-hidden="true" />
|
|
336
237
|
{t().exactlyThis}
|
|
337
238
|
</p>
|
|
338
239
|
</div>
|
|
339
240
|
</PanelDialog.Body>
|
|
340
|
-
<PanelDialog.Footer>
|
|
341
|
-
<div class="cloud-approval__sheet-foot">
|
|
342
|
-
<div class="cloud-approval__foot">
|
|
343
|
-
<Note item={item} />
|
|
344
|
-
<Decision item={item} busy={false} decide={(input) => close(input)} />
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
</PanelDialog.Footer>
|
|
348
241
|
</PanelDialog>
|
|
349
242
|
);
|
|
350
243
|
},
|
|
351
|
-
{
|
|
244
|
+
{
|
|
245
|
+
...panelDialogOptions,
|
|
246
|
+
history: true,
|
|
247
|
+
ariaLabel: approvalMessages
|
|
248
|
+
.resolve([locale])
|
|
249
|
+
.t.approvalFor({ title: approvalCard(cardInput(item), locale, READER_TIME_ZONE).questionText }),
|
|
250
|
+
},
|
|
352
251
|
);
|
|
353
252
|
|
|
354
253
|
function Row(props: {
|
|
@@ -358,53 +257,57 @@ function Row(props: {
|
|
|
358
257
|
busy: boolean;
|
|
359
258
|
failed: boolean;
|
|
360
259
|
decide: (input: ApprovalDecisionInput) => void;
|
|
260
|
+
reopen: () => void;
|
|
361
261
|
}) {
|
|
362
262
|
const t = useMessages();
|
|
363
263
|
const locale = useLocale();
|
|
264
|
+
// The band words the request as the card does: the plain question and Cloud's reason line.
|
|
265
|
+
const card = createMemo(() => approvalCard(cardInput(props.item), locale(), READER_TIME_ZONE));
|
|
364
266
|
const details = async () => {
|
|
365
267
|
const input = await openDetails(props.item, locale());
|
|
366
268
|
if (input) props.decide(input);
|
|
367
269
|
};
|
|
368
|
-
const foot = () => (
|
|
369
|
-
<>
|
|
370
|
-
<Note item={props.item} outcome={props.outcome} failed={props.failed} />
|
|
371
|
-
<Show when={props.outcome} fallback={<Decision item={props.item} busy={props.busy} details={details} decide={props.decide} />}>
|
|
372
|
-
{(outcome) => (
|
|
373
|
-
<div class="cloud-approval__actions">
|
|
374
|
-
<Receipt outcome={outcome()} />
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
</Show>
|
|
378
|
-
</>
|
|
379
|
-
);
|
|
380
270
|
return (
|
|
381
271
|
<Show
|
|
382
272
|
when={props.layout === "band"}
|
|
383
273
|
fallback={
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<Description item={props.item} />
|
|
393
|
-
<Fields item={props.item} />
|
|
394
|
-
<BlockDetails item={props.item} clamp />
|
|
395
|
-
</div>
|
|
396
|
-
<div class="cloud-approval__foot">{foot()}</div>
|
|
397
|
-
</div>
|
|
398
|
-
</article>
|
|
274
|
+
<ApprovalCard
|
|
275
|
+
item={props.item}
|
|
276
|
+
outcome={props.outcome}
|
|
277
|
+
busy={props.busy}
|
|
278
|
+
failed={props.failed}
|
|
279
|
+
decide={props.decide}
|
|
280
|
+
reopen={props.reopen}
|
|
281
|
+
/>
|
|
399
282
|
}
|
|
400
283
|
>
|
|
401
|
-
<article class="cloud-approval-band" data-decided={props.outcome} aria-label={t().approvalFor({ title:
|
|
284
|
+
<article class="cloud-approval-band" data-decided={props.outcome} aria-label={t().approvalFor({ title: card().questionText })}>
|
|
402
285
|
<i class={props.outcome ? outcomeIcon(props.outcome, true) : "ti ti-alert-triangle"} aria-hidden="true" />
|
|
403
286
|
<div class="cloud-approval-band__text">
|
|
404
|
-
<strong>{
|
|
287
|
+
<strong lang={card().locale}>{card().questionText}</strong>
|
|
288
|
+
<span class="cloud-approval-band__reason" lang={card().locale}>
|
|
289
|
+
{card().reason.text}
|
|
290
|
+
</span>
|
|
405
291
|
<Origin item={props.item} withApp={false} />
|
|
406
292
|
</div>
|
|
407
|
-
<div class="cloud-approval__foot">
|
|
293
|
+
<div class="cloud-approval__foot">
|
|
294
|
+
<Show when={props.failed && !props.outcome}>
|
|
295
|
+
<span class="cloud-approval__note" data-tone="danger" role="alert">
|
|
296
|
+
<i class="ti ti-alert-circle" aria-hidden="true" />
|
|
297
|
+
<span>{t().decisionFailed}</span>
|
|
298
|
+
</span>
|
|
299
|
+
</Show>
|
|
300
|
+
<Show
|
|
301
|
+
when={props.outcome}
|
|
302
|
+
fallback={<BandDecision item={props.item} busy={props.busy} details={() => void details()} decide={props.decide} />}
|
|
303
|
+
>
|
|
304
|
+
{(outcome) => (
|
|
305
|
+
<div class="cloud-approval__actions">
|
|
306
|
+
<Receipt outcome={outcome()} />
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</Show>
|
|
310
|
+
</div>
|
|
408
311
|
</article>
|
|
409
312
|
</Show>
|
|
410
313
|
);
|
|
@@ -528,6 +431,7 @@ export default function Approvals(props: ApprovalsProps) {
|
|
|
528
431
|
busy={busy().has(item.id)}
|
|
529
432
|
failed={failed().has(item.id)}
|
|
530
433
|
decide={(input) => void decide(item, input)}
|
|
434
|
+
reopen={() => setFailed((current) => toggle(current, item.id, false))}
|
|
531
435
|
/>
|
|
532
436
|
)}
|
|
533
437
|
</For>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { capabilityQuestionText, capabilityUnknownText } from "../_internal/capability-sentences";
|
|
2
|
+
import { type AiStoredActionReview, isLegacyActionReview, storedActionReview } from "../ai/action-review";
|
|
3
|
+
import { type ActionCard, legacyActionCard, reviewActionCard } from "../ai/chat/action-card";
|
|
4
|
+
import { aiChatMessages } from "../ai/chat/messages";
|
|
5
|
+
import type { CapabilityActionReviewData } from "../contracts/capabilities";
|
|
6
|
+
|
|
7
|
+
export type ApprovalCardInput = {
|
|
8
|
+
sentence: string;
|
|
9
|
+
action: string;
|
|
10
|
+
appName: string;
|
|
11
|
+
appIcon?: string | null;
|
|
12
|
+
review: CapabilityActionReviewData | AiStoredActionReview | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const lines = (item: ApprovalCardInput) =>
|
|
16
|
+
item.sentence
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
/** The first line names the app and the Action; the app already shows as the row's icon and origin. */
|
|
22
|
+
const approvalTitle = (item: ApprovalCardInput): string => {
|
|
23
|
+
const first = lines(item)[0] ?? item.action;
|
|
24
|
+
const prefix = `${item.appName}: `;
|
|
25
|
+
return first.startsWith(prefix) && first.length > prefix.length ? first.slice(prefix.length) : first;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Without a review, the sentence's later `Label: value` lines are what the person approves. */
|
|
29
|
+
const sentenceRows = (item: ApprovalCardInput) =>
|
|
30
|
+
lines(item)
|
|
31
|
+
.slice(1)
|
|
32
|
+
.map((line) => {
|
|
33
|
+
const match = /^([^:]{1,40}):\s+(\S.*)$/.exec(line);
|
|
34
|
+
return match ? { label: match[1]!, value: match[2] ?? "" } : { label: "", value: line };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The same card as the chat: a new review gives the app's sentence, reason, and preview; an old review its literal
|
|
39
|
+
* message and rows; a request without a review its sentence. Times show in the reader's zone (UTC until the page
|
|
40
|
+
* passes one, the same on the server and in the first browser render).
|
|
41
|
+
*/
|
|
42
|
+
export const approvalCard = (item: ApprovalCardInput, locale: string, timeZone: string): ActionCard => {
|
|
43
|
+
const app = { name: item.appName, icon: item.appIcon ?? "ti ti-shield-check" };
|
|
44
|
+
const context = { locale, timeZone, app };
|
|
45
|
+
const review = item.review ? ("legacy" in item.review ? item.review : storedActionReview(item.review)) : null;
|
|
46
|
+
if (review && !isLegacyActionReview(review)) return reviewActionCard(review, context);
|
|
47
|
+
const title = approvalTitle(item);
|
|
48
|
+
const t = aiChatMessages(locale);
|
|
49
|
+
return legacyActionCard(
|
|
50
|
+
{
|
|
51
|
+
question: review?.legacy.question ?? capabilityQuestionText(title, locale),
|
|
52
|
+
rows: review?.legacy.rows ?? sentenceRows(item),
|
|
53
|
+
...(review?.legacy.markdown ? { markdown: review.legacy.markdown } : {}),
|
|
54
|
+
...(review?.legacy.links ? { links: review.legacy.links } : {}),
|
|
55
|
+
subject: title,
|
|
56
|
+
title,
|
|
57
|
+
outcome: {
|
|
58
|
+
running: t.receiptRunning({ title }),
|
|
59
|
+
done: t.receiptDone({ title }),
|
|
60
|
+
rejected: t.receiptRejected({ title }),
|
|
61
|
+
notRun: t.receiptNotRun({ title, stopped: false }),
|
|
62
|
+
unknown: capabilityUnknownText(title, locale),
|
|
63
|
+
unknownStill: capabilityUnknownText(title, locale, true),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
context,
|
|
67
|
+
);
|
|
68
|
+
};
|