@k2b/cloud 0.29.0 → 0.30.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/package.json +5 -3
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +111 -3
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +10 -1
- package/src/_internal/registry-validation.ts +5 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +203 -185
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +5 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +12 -26
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +4 -4
- package/src/ai/chat/turn-view.tsx +141 -69
- package/src/ai/code-runtime-tools.ts +59 -44
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +19 -21
- package/src/ai/index.ts +9 -1
- package/src/ai/migrate.ts +59 -12
- package/src/ai/protocol.ts +9 -4
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +20 -13
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/types.ts +10 -2
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +259 -100
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/widgets.ts +28 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/admin/outgoing-mail.ts +30 -2
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/outgoing-mail.ts +56 -11
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +20 -0
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/services/accounts/users.ts +15 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/shared/network-address.ts +1 -0
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +142 -0
- package/src/styles/utilities-markdown-table.css +5 -1
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
package/src/ai/chat/blocks.tsx
CHANGED
|
@@ -2,12 +2,15 @@ import { dates } from "@k2b/stdlib";
|
|
|
2
2
|
import { mutation } from "@k2b/stdlib/solid";
|
|
3
3
|
import { Button, ButtonLink, Chat, isStructuredDataValue, MarkdownView, SplitButton, StructuredDataPreview, useLocale } from "@k2b/ui";
|
|
4
4
|
import { createSignal, createUniqueId, For, type JSX, Match, Show, Switch, untrack } from "solid-js";
|
|
5
|
+
import { capabilityApprovalReason, capabilityApprovalReasonLabel } from "../../_internal/capability-sentences";
|
|
5
6
|
import type { CapabilityActionReview } from "../../contracts/capabilities";
|
|
6
7
|
import { markdown } from "../../shared";
|
|
7
8
|
import type { AiTurnBlock } from "../protocol";
|
|
9
|
+
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
8
10
|
import { hasSpecializedBuiltinToolView, SpecializedBuiltinToolBlock } from "./builtin-tools";
|
|
9
11
|
import { hasCapabilityTable } from "./capability-result";
|
|
10
12
|
import { CapabilityTablePreview } from "./capability-table";
|
|
13
|
+
import { CloudChartBlock } from "./chart-block";
|
|
11
14
|
import { PresentToolBlock } from "./file-tools";
|
|
12
15
|
import { useAiChatActions } from "./message-actions";
|
|
13
16
|
import {
|
|
@@ -18,6 +21,7 @@ import {
|
|
|
18
21
|
fetchFileErrorPresentation,
|
|
19
22
|
formatToolDetailText,
|
|
20
23
|
isCardToolName,
|
|
24
|
+
isChartToolName,
|
|
21
25
|
isRecord,
|
|
22
26
|
isSurveyToolName,
|
|
23
27
|
isTextEditorToolName,
|
|
@@ -183,7 +187,14 @@ function ToolResultDisclosure(props: {
|
|
|
183
187
|
);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
|
|
190
|
+
/** A decision the server accepted, for the call it answered. */
|
|
191
|
+
export type AiApprovalDecision = { callId: string; approved: boolean };
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The pending approval: a calm tinted card that says what will happen, with its fields and the decision. `onDecided`
|
|
195
|
+
* hears an accepted decision with the call it answered, so the turn can show its receipt in the card's place right away.
|
|
196
|
+
*/
|
|
197
|
+
export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onDecided?: (decision: AiApprovalDecision) => void }) {
|
|
187
198
|
const actions = useAiChatActions();
|
|
188
199
|
const locale = useLocale();
|
|
189
200
|
const t = () => aiChatMessages(locale());
|
|
@@ -192,21 +203,34 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
192
203
|
const actionDisabled = () => actions.actionDisabled?.() ?? false;
|
|
193
204
|
const [submitted, setSubmitted] = createSignal(false);
|
|
194
205
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
|
206
|
+
// The call this card answered: a later approval for the same block may already have arrived when the server accepts.
|
|
207
|
+
let decision: AiApprovalDecision | null = null;
|
|
195
208
|
const approval = mutation.create<void, { approved: boolean; remember?: "always" }>({
|
|
196
209
|
mutation: async (input) => {
|
|
197
210
|
if (!actions.onApproval) throw new Error("Approval is unavailable.");
|
|
198
211
|
await actions.onApproval(request(), input);
|
|
199
212
|
},
|
|
200
|
-
onSuccess: () =>
|
|
213
|
+
onSuccess: () => {
|
|
214
|
+
setSubmitted(true);
|
|
215
|
+
if (decision) props.onDecided?.(decision);
|
|
216
|
+
},
|
|
201
217
|
});
|
|
202
218
|
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
203
|
-
if (
|
|
219
|
+
if (actionDisabled() || approval.loading()) return;
|
|
220
|
+
decision = { callId: props.block.callId, approved: input.approved };
|
|
221
|
+
void approval.mutate(input);
|
|
204
222
|
};
|
|
205
|
-
|
|
223
|
+
// The heading says what will happen in the owning app's words; the app or the assistant name stays beside it.
|
|
224
|
+
const title = () => toolSubject(props.block, locale()) ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale());
|
|
206
225
|
const ownerName = () => props.block.presentation?.appName ?? t().assistant;
|
|
226
|
+
const capability = () => props.block.presentation?.kind === "capability";
|
|
227
|
+
// The model's own reason, labelled as such, only where Cloud offered it; it adds to the app's sentence and never replaces it.
|
|
228
|
+
const reason = () => (props.block.presentation?.approvalReason ? capabilityApprovalReason(props.block.args) : null);
|
|
207
229
|
const description = () => {
|
|
208
230
|
const reviewMessage = props.block.approval?.review?.message.trim();
|
|
209
231
|
if (reviewMessage) return reviewMessage;
|
|
232
|
+
// An app Action's approval text repeats its heading and reason for text-only readers; the card shows them itself.
|
|
233
|
+
if (capability()) return null;
|
|
210
234
|
const message = props.block.approval?.message?.trim();
|
|
211
235
|
if (!message) return null;
|
|
212
236
|
const duplicateTitle = props.block.presentation ? `${props.block.presentation.appName}: ${props.block.presentation.title}` : "";
|
|
@@ -239,199 +263,174 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
239
263
|
download: t().download,
|
|
240
264
|
})[rel];
|
|
241
265
|
const detailData = () => (isStructuredDataValue(props.block.args) ? props.block.args : undefined);
|
|
242
|
-
const appAccent = () => props.block.presentation?.appAccent;
|
|
243
266
|
const detailsId = `approval-details-${props.block.callId}`;
|
|
244
267
|
return (
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
class={`
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
<
|
|
287
|
-
<Show
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
268
|
+
<section class="ai-approval" aria-label={t().approvalRequired({ title: title() })}>
|
|
269
|
+
<div class="ai-approval__head">
|
|
270
|
+
<i class={`ai-approval__icon ${aiToolIcon(props.block.name, props.block.presentation?.appIcon)}`} aria-hidden="true" />
|
|
271
|
+
<div class="min-w-0">
|
|
272
|
+
<h3 class="ai-approval__title">{title()}</h3>
|
|
273
|
+
<p class="ai-approval__sub">
|
|
274
|
+
{ownerName()} · {t().approvalRunsAfter}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<Show when={reviewLines().length > 0}>
|
|
279
|
+
<div class="ai-approval__text">
|
|
280
|
+
<For each={reviewLines()}>
|
|
281
|
+
{(line) => (
|
|
282
|
+
<p class="whitespace-pre-wrap">
|
|
283
|
+
<Show when={line.label}>{(label) => <strong class="ai-approval__label">{label()}: </strong>}</Show>
|
|
284
|
+
{line.value}
|
|
285
|
+
</p>
|
|
286
|
+
)}
|
|
287
|
+
</For>
|
|
288
|
+
</div>
|
|
289
|
+
</Show>
|
|
290
|
+
<Show when={props.block.approval?.review}>
|
|
291
|
+
<p class="ai-approval__text whitespace-pre-wrap">{description()}</p>
|
|
292
|
+
<Show when={inlineReviewDetails().length > 0}>
|
|
293
|
+
<dl class="ai-approval__fields">
|
|
294
|
+
<For each={inlineReviewDetails()}>
|
|
295
|
+
{(detail) => (
|
|
296
|
+
<>
|
|
297
|
+
<dt>{detail.label}</dt>
|
|
298
|
+
<dd class="whitespace-pre-wrap">
|
|
299
|
+
<ReviewDetailValue detail={detail} />
|
|
300
|
+
</dd>
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</For>
|
|
304
|
+
</dl>
|
|
305
|
+
</Show>
|
|
306
|
+
<For each={blockReviewDetails()}>
|
|
307
|
+
{(detail) => (
|
|
308
|
+
<section class="ai-approval__text min-w-0" aria-label={detail.label}>
|
|
309
|
+
<h4 class="ai-approval__label mb-1">{detail.label}</h4>
|
|
310
|
+
<Show
|
|
311
|
+
when={detail.format === "markdown"}
|
|
312
|
+
fallback={
|
|
313
|
+
<pre
|
|
314
|
+
class="ai-approval__preview whitespace-pre-wrap break-words font-sans"
|
|
315
|
+
role="region"
|
|
316
|
+
tabIndex={0}
|
|
317
|
+
aria-label={t().contentOf({ label: detail.label })}
|
|
318
|
+
>
|
|
319
|
+
<ReviewDetailValue detail={detail} />
|
|
320
|
+
</pre>
|
|
321
|
+
}
|
|
322
|
+
>
|
|
323
|
+
<div class="ai-approval__preview" role="region" tabIndex={0} aria-label={detail.label}>
|
|
324
|
+
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
325
|
+
</div>
|
|
300
326
|
</Show>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
>
|
|
318
|
-
<div
|
|
319
|
-
class="-mx-2 max-h-72 overflow-auto px-2 text-xs text-secondary"
|
|
320
|
-
role="region"
|
|
321
|
-
tabIndex={0}
|
|
322
|
-
aria-label={detail.label}
|
|
323
|
-
>
|
|
324
|
-
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
325
|
-
</div>
|
|
326
|
-
</Show>
|
|
327
|
-
</section>
|
|
328
|
-
)}
|
|
329
|
-
</For>
|
|
330
|
-
</div>
|
|
327
|
+
</section>
|
|
328
|
+
)}
|
|
329
|
+
</For>
|
|
330
|
+
</Show>
|
|
331
|
+
<Show when={reason()}>
|
|
332
|
+
{(text) => (
|
|
333
|
+
<p class="ai-approval__text whitespace-pre-wrap">
|
|
334
|
+
<strong class="ai-approval__label">{capabilityApprovalReasonLabel(locale())}: </strong>
|
|
335
|
+
{text()}
|
|
336
|
+
</p>
|
|
337
|
+
)}
|
|
338
|
+
</Show>
|
|
339
|
+
<Show when={detailsOpen()}>
|
|
340
|
+
<div id={detailsId} class="w-full min-w-0" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
341
|
+
<Show when={detailData()} fallback={<pre class="ai-approval__preview text-[11px]">{jsonPreview(props.block.args)}</pre>}>
|
|
342
|
+
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
331
343
|
</Show>
|
|
332
344
|
</div>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
</
|
|
349
|
-
</
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
</
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
345
|
+
</Show>
|
|
346
|
+
{/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
|
|
347
|
+
<footer class="ai-approval__foot" data-ai-approval-footer>
|
|
348
|
+
{/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
|
|
349
|
+
<span class="k2b-sr-only" role="status" aria-live="polite">
|
|
350
|
+
{approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
|
|
351
|
+
</span>
|
|
352
|
+
<Show when={reviewLinks().length > 0}>
|
|
353
|
+
<nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
|
|
354
|
+
<For each={reviewLinks()}>
|
|
355
|
+
{(link) => (
|
|
356
|
+
<ButtonLink href={link.href} target="_blank" rel="noopener noreferrer" size="xs" variant="ghost">
|
|
357
|
+
{link.title ?? reviewLinkTitle(link.rel)}
|
|
358
|
+
</ButtonLink>
|
|
359
|
+
)}
|
|
360
|
+
</For>
|
|
361
|
+
</nav>
|
|
362
|
+
</Show>
|
|
363
|
+
<Show when={approval.error()}>
|
|
364
|
+
<p class="text-xs text-red-700 dark:text-red-300" role="alert">
|
|
365
|
+
{t().approvalFailed}
|
|
366
|
+
</p>
|
|
367
|
+
</Show>
|
|
368
|
+
<div class="ai-approval__actions">
|
|
369
|
+
<Show
|
|
370
|
+
when={pending()}
|
|
371
|
+
fallback={
|
|
372
|
+
<span class="ai-approval__state">
|
|
373
|
+
{title()} · {props.block.status === "rejected" ? t().rejected : t().approved}
|
|
374
|
+
</span>
|
|
375
|
+
}
|
|
376
|
+
>
|
|
377
|
+
<Show when={actions.onApproval} fallback={<span class="ai-approval__state">{t().approvalUnavailable}</span>}>
|
|
378
|
+
<Show
|
|
379
|
+
when={!actionDisabled()}
|
|
380
|
+
fallback={
|
|
381
|
+
<span class="ai-approval__state">
|
|
382
|
+
<i class="ti ti-player-stop" aria-hidden="true" />
|
|
383
|
+
{t().stoppingResponse}
|
|
384
|
+
</span>
|
|
385
|
+
}
|
|
386
|
+
>
|
|
365
387
|
<Show
|
|
366
|
-
when={!
|
|
388
|
+
when={!submitted()}
|
|
367
389
|
fallback={
|
|
368
|
-
<span class="
|
|
369
|
-
<i class="ti ti-
|
|
370
|
-
{t().
|
|
390
|
+
<span class="ai-approval__state">
|
|
391
|
+
<i class="ti ti-check" aria-hidden="true" />
|
|
392
|
+
{t().submitted}
|
|
371
393
|
</span>
|
|
372
394
|
}
|
|
373
395
|
>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
396
|
+
{/* The buttons stay while the decision is sent, so the card does not change its height before it
|
|
397
|
+
becomes its receipt. */}
|
|
398
|
+
<Button size="sm" variant="ghost" disabled={approval.loading()} onClick={() => submit({ approved: false })}>
|
|
399
|
+
{t().reject}
|
|
400
|
+
</Button>
|
|
401
|
+
<SplitButton
|
|
402
|
+
size="sm"
|
|
403
|
+
variant="ai"
|
|
404
|
+
loading={approval.loading()}
|
|
405
|
+
onClick={() => submit({ approved: true })}
|
|
406
|
+
menuLabel={t().moreOptions({ title: title() })}
|
|
407
|
+
menuPosition="bottom-right"
|
|
408
|
+
items={[
|
|
409
|
+
{
|
|
410
|
+
label: detailsOpen() ? t().hideDetails : t().details,
|
|
411
|
+
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
412
|
+
action: () => setDetailsOpen((open) => !open),
|
|
413
|
+
},
|
|
414
|
+
...(props.block.approval?.allowAlways
|
|
415
|
+
? [
|
|
416
|
+
{
|
|
417
|
+
label: t().alwaysApprove,
|
|
418
|
+
icon: "ti ti-shield-check",
|
|
419
|
+
action: () => submit({ approved: true, remember: "always" }),
|
|
420
|
+
},
|
|
421
|
+
]
|
|
422
|
+
: []),
|
|
423
|
+
]}
|
|
382
424
|
>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{t().reject}
|
|
386
|
-
</Button>
|
|
387
|
-
<SplitButton
|
|
388
|
-
size="xs"
|
|
389
|
-
variant="ai"
|
|
390
|
-
onClick={() => submit({ approved: true })}
|
|
391
|
-
menuLabel={t().moreOptions({ title: title() })}
|
|
392
|
-
menuPosition="bottom-right"
|
|
393
|
-
items={[
|
|
394
|
-
{
|
|
395
|
-
label: detailsOpen() ? t().hideDetails : t().details,
|
|
396
|
-
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
397
|
-
action: () => setDetailsOpen((open) => !open),
|
|
398
|
-
},
|
|
399
|
-
...(props.block.approval?.allowAlways
|
|
400
|
-
? [
|
|
401
|
-
{
|
|
402
|
-
label: t().alwaysApprove,
|
|
403
|
-
icon: "ti ti-shield-check",
|
|
404
|
-
action: () => submit({ approved: true, remember: "always" }),
|
|
405
|
-
},
|
|
406
|
-
]
|
|
407
|
-
: []),
|
|
408
|
-
]}
|
|
409
|
-
>
|
|
410
|
-
{title()}
|
|
411
|
-
</SplitButton>
|
|
412
|
-
</div>
|
|
413
|
-
</Show>
|
|
425
|
+
{title()}
|
|
426
|
+
</SplitButton>
|
|
414
427
|
</Show>
|
|
415
428
|
</Show>
|
|
416
429
|
</Show>
|
|
417
|
-
</div>
|
|
418
|
-
</footer>
|
|
419
|
-
</section>
|
|
420
|
-
<Show when={detailsOpen()}>
|
|
421
|
-
<div id={detailsId} class="mt-2 w-full" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
422
|
-
<Show
|
|
423
|
-
when={detailData()}
|
|
424
|
-
fallback={
|
|
425
|
-
<pre class="max-h-40 overflow-auto rounded-md bg-white/55 p-2 text-[11px] text-primary dark:bg-black/20">
|
|
426
|
-
{jsonPreview(props.block.args)}
|
|
427
|
-
</pre>
|
|
428
|
-
}
|
|
429
|
-
>
|
|
430
|
-
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
431
430
|
</Show>
|
|
432
431
|
</div>
|
|
433
|
-
</
|
|
434
|
-
</
|
|
432
|
+
</footer>
|
|
433
|
+
</section>
|
|
435
434
|
);
|
|
436
435
|
}
|
|
437
436
|
|
|
@@ -537,11 +536,11 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
537
536
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
538
537
|
const locale = useLocale();
|
|
539
538
|
const presentation = () => props.block.presentation;
|
|
540
|
-
const title = () => presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
539
|
+
const title = () => toolSubject(props.block, locale()) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
541
540
|
return (
|
|
542
541
|
<Chat.Activity
|
|
543
542
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
544
|
-
label={aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
543
|
+
label={toolOutcome(props.block, "rejected", locale()) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
545
544
|
accent={presentation()?.appAccent}
|
|
546
545
|
/>
|
|
547
546
|
);
|
|
@@ -713,6 +712,9 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
713
712
|
<Match when={isCardToolName(props.block.name) && !props.block.isError}>
|
|
714
713
|
<CloudCardBlock args={props.block.args} />
|
|
715
714
|
</Match>
|
|
715
|
+
<Match when={isChartToolName(props.block.name) && !props.block.isError}>
|
|
716
|
+
<CloudChartBlock args={props.block.args} completed={status() === "completed"} />
|
|
717
|
+
</Match>
|
|
716
718
|
<Match when={isSurveyToolName(props.block.name) && !props.block.isError}>
|
|
717
719
|
<SurveyToolView turnId={props.turnId} block={props.block} active={props.active} />
|
|
718
720
|
</Match>
|
|
@@ -810,7 +812,22 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
810
812
|
const locale = useLocale();
|
|
811
813
|
const t = () => aiChatMessages(locale());
|
|
812
814
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
813
|
-
|
|
815
|
+
/**
|
|
816
|
+
* A call that acts on something reads as its sentence, which already names what it acts on: what it will do while
|
|
817
|
+
* it runs or waits, what it did once done, and what did not happen when it was rejected.
|
|
818
|
+
*/
|
|
819
|
+
const sentence = () => {
|
|
820
|
+
const status = props.block.status;
|
|
821
|
+
const outcome =
|
|
822
|
+
status === "completed" && !props.block.isError
|
|
823
|
+
? toolOutcome(props.block, "done", locale())
|
|
824
|
+
: status === "rejected"
|
|
825
|
+
? toolOutcome(props.block, "rejected", locale())
|
|
826
|
+
: null;
|
|
827
|
+
return outcome ?? toolSubject(props.block, locale());
|
|
828
|
+
};
|
|
829
|
+
const detail = () =>
|
|
830
|
+
sentence() ? undefined : [args().path, args().name, args().query, args().title].find((value) => typeof value === "string");
|
|
814
831
|
const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
|
|
815
832
|
const state = () =>
|
|
816
833
|
waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
|
|
@@ -838,7 +855,7 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
838
855
|
return (
|
|
839
856
|
<AiToolActivity
|
|
840
857
|
blockId={props.block.id}
|
|
841
|
-
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
858
|
+
label={sentence() ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
842
859
|
description={[
|
|
843
860
|
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
844
861
|
typeof detail() === "string" ? String(detail()) : "",
|
|
@@ -849,6 +866,7 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
849
866
|
.join(" · ")}
|
|
850
867
|
icon={icon()}
|
|
851
868
|
busy={props.busy && props.block.status === "running"}
|
|
869
|
+
bodyInset={false}
|
|
852
870
|
renderBody={() => (
|
|
853
871
|
<div class="flex min-w-0 flex-col gap-2">
|
|
854
872
|
<div
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ChartExplorer, type ChartExplorerColumn, prepareChartSnapshot, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createMemo, Show } from "solid-js";
|
|
3
|
+
import {
|
|
4
|
+
type CloudAiChartColumn,
|
|
5
|
+
type CloudAiChartRow,
|
|
6
|
+
cloudAiChartMarkKey,
|
|
7
|
+
cloudAiChartRenderOptions,
|
|
8
|
+
cloudAiChartRows,
|
|
9
|
+
} from "../chart-block";
|
|
10
|
+
import { parseCloudAiChartInput } from "../default-tool-contracts";
|
|
11
|
+
import { aiChatMessages } from "./messages";
|
|
12
|
+
|
|
13
|
+
const tooltip = (columns: CloudAiChartColumn[], row: CloudAiChartRow) => {
|
|
14
|
+
const [first, ...rest] = columns;
|
|
15
|
+
const named = first && !first.numeric;
|
|
16
|
+
return {
|
|
17
|
+
title: named ? row.cells[first.id] : undefined,
|
|
18
|
+
rows: (named ? rest : columns).map((column) => ({ label: column.label, value: row.cells[column.id] ?? "" })),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One `chart` tool call: the shared chart with its data table and Copy data. The block takes its full size with its
|
|
24
|
+
* first frame and keeps it; switching to the table scrolls inside the same height. A call whose arguments are not a
|
|
25
|
+
* valid chart shows nothing while it runs, because the model gets the validation error and the call fails.
|
|
26
|
+
*/
|
|
27
|
+
export function CloudChartBlock(props: { args: unknown; completed: boolean }) {
|
|
28
|
+
const locale = useLocale();
|
|
29
|
+
// Live updates replace the block with equal arguments; only a real change redraws.
|
|
30
|
+
const source = createMemo(() => JSON.stringify(props.args ?? null));
|
|
31
|
+
const chart = createMemo(() => {
|
|
32
|
+
// The tool schema drew the chart once to accept it; here it is drawn once more, for display, and the table is
|
|
33
|
+
// filled from that drawing.
|
|
34
|
+
const input = parseCloudAiChartInput(JSON.parse(source()));
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
try {
|
|
37
|
+
const rows = cloudAiChartRows(input, locale());
|
|
38
|
+
const snapshot = prepareChartSnapshot(cloudAiChartRenderOptions(input, locale()), {
|
|
39
|
+
key: ({ datum }) => cloudAiChartMarkKey(datum),
|
|
40
|
+
tooltip: ({ datum }) => {
|
|
41
|
+
const row = rows.add(datum);
|
|
42
|
+
return tooltip(rows.columns(), row);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return snapshot.marks.length > 0 ? { input, table: rows.table(), snapshot } : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return (
|
|
51
|
+
<Show
|
|
52
|
+
when={chart()}
|
|
53
|
+
fallback={
|
|
54
|
+
<Show when={props.completed}>
|
|
55
|
+
<p class="text-sm text-secondary">{aiChatMessages(locale()).chartUnavailable}</p>
|
|
56
|
+
</Show>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{(value) => (
|
|
60
|
+
<ChartExplorer
|
|
61
|
+
title={value().input.title}
|
|
62
|
+
description={value().input.subtitle ? <span class="text-sm text-secondary">{value().input.subtitle}</span> : undefined}
|
|
63
|
+
height={value().input.kind === "sparkline" ? "8rem" : "18rem"}
|
|
64
|
+
data={{ chart: value().snapshot, rows: value().table.rows }}
|
|
65
|
+
columns={value().table.columns.map(
|
|
66
|
+
(column): ChartExplorerColumn<CloudAiChartRow> => ({
|
|
67
|
+
id: column.id,
|
|
68
|
+
label: column.label,
|
|
69
|
+
value: (row) => row.cells[column.id] ?? "",
|
|
70
|
+
sortValue: (row) => row.values[column.id] ?? null,
|
|
71
|
+
align: column.numeric ? "right" : "left",
|
|
72
|
+
}),
|
|
73
|
+
)}
|
|
74
|
+
renderDetails={false}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</Show>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -11,15 +11,18 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
11
11
|
* controller does. `window.emit(event)` applies one event,
|
|
12
12
|
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
13
|
* `window.reconnect(on)` sets whether the stream reconnects,
|
|
14
|
-
* `window.continued` lists the messages that **Continue** sent,
|
|
14
|
+
* `window.continued` lists the messages that **Continue** sent,
|
|
15
|
+
* `window.approvals` lists the approval decisions, and
|
|
15
16
|
* `window.opened` lists the chat files the timeline opened. Chat files load from `/files?path=`.
|
|
16
17
|
*/
|
|
17
18
|
const [state, setState] = createStore(emptyProjection());
|
|
18
19
|
const [reconnecting, setReconnecting] = createSignal(false);
|
|
19
20
|
const continued: string[] = [];
|
|
20
21
|
const opened: string[] = [];
|
|
22
|
+
const approvals: unknown[] = [];
|
|
21
23
|
Object.assign(window, {
|
|
22
24
|
continued,
|
|
25
|
+
approvals,
|
|
23
26
|
opened,
|
|
24
27
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
25
28
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
@@ -38,6 +41,7 @@ render(
|
|
|
38
41
|
<AiChatActionsProvider
|
|
39
42
|
actions={{
|
|
40
43
|
onContinueTurn: (message) => void continued.push(message),
|
|
44
|
+
onApproval: async (request, input) => void approvals.push({ callId: request.callId, ...input }),
|
|
41
45
|
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
42
46
|
onOpenFile: (path) => void opened.push(path),
|
|
43
47
|
}}
|
|
@@ -311,6 +311,8 @@ export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
311
311
|
|
|
312
312
|
export const isCardToolName = (name: string) => name === "card" || name === "cloud_card";
|
|
313
313
|
|
|
314
|
+
export const isChartToolName = (name: string) => name === "chart";
|
|
315
|
+
|
|
314
316
|
export const isSurveyToolName = (name: string) => name === "survey" || name === "cloud_survey";
|
|
315
317
|
|
|
316
318
|
export const isTextEditorToolName = (name: string) => name === "text_editor" || name === "cloud_text_editor";
|
|
@@ -326,6 +328,7 @@ export const displayToolName = (name: string, locale: string) => {
|
|
|
326
328
|
const BUILT_IN_TOOL_ICONS = new Map<string, string>([
|
|
327
329
|
["card", "ti ti-layout-cards"],
|
|
328
330
|
["cloud_card", "ti ti-layout-cards"],
|
|
331
|
+
["chart", "ti ti-chart-bar"],
|
|
329
332
|
["survey", "ti ti-forms"],
|
|
330
333
|
["cloud_survey", "ti ti-forms"],
|
|
331
334
|
["text_editor", "ti ti-edit"],
|