@k2b/cloud 0.29.1 → 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 +3 -2
- 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 +45 -9
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +8 -2
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -2
- package/src/ai/chat/turn-view.tsx +29 -11
- 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/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/tool-sentences.ts +38 -0
- package/src/ai/types.ts +8 -1
- package/src/api/admin-ai-skills.ts +35 -16
- 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/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/capabilities.ts +24 -0
- 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/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/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,
|
|
@@ -216,11 +220,17 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
216
220
|
decision = { callId: props.block.callId, approved: input.approved };
|
|
217
221
|
void approval.mutate(input);
|
|
218
222
|
};
|
|
219
|
-
|
|
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());
|
|
220
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);
|
|
221
229
|
const description = () => {
|
|
222
230
|
const reviewMessage = props.block.approval?.review?.message.trim();
|
|
223
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;
|
|
224
234
|
const message = props.block.approval?.message?.trim();
|
|
225
235
|
if (!message) return null;
|
|
226
236
|
const duplicateTitle = props.block.presentation ? `${props.block.presentation.appName}: ${props.block.presentation.title}` : "";
|
|
@@ -259,10 +269,10 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
259
269
|
<div class="ai-approval__head">
|
|
260
270
|
<i class={`ai-approval__icon ${aiToolIcon(props.block.name, props.block.presentation?.appIcon)}`} aria-hidden="true" />
|
|
261
271
|
<div class="min-w-0">
|
|
262
|
-
<h3 class="ai-approval__title">
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
272
|
+
<h3 class="ai-approval__title">{title()}</h3>
|
|
273
|
+
<p class="ai-approval__sub">
|
|
274
|
+
{ownerName()} · {t().approvalRunsAfter}
|
|
275
|
+
</p>
|
|
266
276
|
</div>
|
|
267
277
|
</div>
|
|
268
278
|
<Show when={reviewLines().length > 0}>
|
|
@@ -318,6 +328,14 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
318
328
|
)}
|
|
319
329
|
</For>
|
|
320
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>
|
|
321
339
|
<Show when={detailsOpen()}>
|
|
322
340
|
<div id={detailsId} class="w-full min-w-0" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
323
341
|
<Show when={detailData()} fallback={<pre class="ai-approval__preview text-[11px]">{jsonPreview(props.block.args)}</pre>}>
|
|
@@ -518,11 +536,11 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
518
536
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
519
537
|
const locale = useLocale();
|
|
520
538
|
const presentation = () => props.block.presentation;
|
|
521
|
-
const title = () => presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
539
|
+
const title = () => toolSubject(props.block, locale()) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
522
540
|
return (
|
|
523
541
|
<Chat.Activity
|
|
524
542
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
525
|
-
label={aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
543
|
+
label={toolOutcome(props.block, "rejected", locale()) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
526
544
|
accent={presentation()?.appAccent}
|
|
527
545
|
/>
|
|
528
546
|
);
|
|
@@ -694,6 +712,9 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
694
712
|
<Match when={isCardToolName(props.block.name) && !props.block.isError}>
|
|
695
713
|
<CloudCardBlock args={props.block.args} />
|
|
696
714
|
</Match>
|
|
715
|
+
<Match when={isChartToolName(props.block.name) && !props.block.isError}>
|
|
716
|
+
<CloudChartBlock args={props.block.args} completed={status() === "completed"} />
|
|
717
|
+
</Match>
|
|
697
718
|
<Match when={isSurveyToolName(props.block.name) && !props.block.isError}>
|
|
698
719
|
<SurveyToolView turnId={props.turnId} block={props.block} active={props.active} />
|
|
699
720
|
</Match>
|
|
@@ -791,7 +812,22 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
791
812
|
const locale = useLocale();
|
|
792
813
|
const t = () => aiChatMessages(locale());
|
|
793
814
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
794
|
-
|
|
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");
|
|
795
831
|
const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
|
|
796
832
|
const state = () =>
|
|
797
833
|
waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
|
|
@@ -819,7 +855,7 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
819
855
|
return (
|
|
820
856
|
<AiToolActivity
|
|
821
857
|
blockId={props.block.id}
|
|
822
|
-
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
858
|
+
label={sentence() ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
823
859
|
description={[
|
|
824
860
|
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
825
861
|
typeof detail() === "string" ? String(detail()) : "",
|
|
@@ -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
|
+
}
|
|
@@ -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"],
|
package/src/ai/chat/messages.ts
CHANGED
|
@@ -9,6 +9,7 @@ const messages = i18n.define({
|
|
|
9
9
|
tableLinks: "Links",
|
|
10
10
|
tableOpen: "Open",
|
|
11
11
|
tableEmpty: "No matching rows.",
|
|
12
|
+
chartUnavailable: "This chart cannot be shown.",
|
|
12
13
|
tableCapped: "Showing the first 100 returned rows. The full tool result remains available in details.",
|
|
13
14
|
tableMore: "More rows are available. Ask for the next page or open the result in the application.",
|
|
14
15
|
assistantTurns: "Assistant turns",
|
|
@@ -84,6 +85,7 @@ const messages = i18n.define({
|
|
|
84
85
|
stepDeliveringFile: "Delivering a file",
|
|
85
86
|
stepPreparingView: "Preparing a view",
|
|
86
87
|
stepPreparingCard: "Preparing a card",
|
|
88
|
+
stepPreparingChart: "Preparing a chart",
|
|
87
89
|
stepCompacting: "Summarizing the conversation",
|
|
88
90
|
stepTool: "Using a tool",
|
|
89
91
|
stepFailed: "failed",
|
|
@@ -98,7 +100,7 @@ const messages = i18n.define({
|
|
|
98
100
|
groupLoad: "Loaded tools and guidance",
|
|
99
101
|
groupFailed: ({ count }: { count: number }) => `${count} failed`,
|
|
100
102
|
groupRejected: ({ count }: { count: number }) => `${count} rejected`,
|
|
101
|
-
receiptDone: ({ title }: { title: string }) =>
|
|
103
|
+
receiptDone: ({ title }: { title: string }) => `Done: ${title}`,
|
|
102
104
|
receiptApproved: ({ title }: { title: string }) => `Approved: ${title}`,
|
|
103
105
|
receiptRunning: ({ title }: { title: string }) => `Running: ${title}`,
|
|
104
106
|
approvalNeeded: ({ title }: { title: string }) => `Approval needed: ${title}`,
|
|
@@ -106,6 +108,7 @@ const messages = i18n.define({
|
|
|
106
108
|
receiptRejected: ({ title }: { title: string }) => `Rejected: ${title}`,
|
|
107
109
|
receiptFailed: ({ title }: { title: string }) => `Failed: ${title}`,
|
|
108
110
|
receiptNotRun: ({ title, stopped }: { title: string; stopped: boolean }) => `Not run: ${title}${stopped ? " · stopped" : ""}`,
|
|
111
|
+
receiptNotRunSentence: ({ sentence, stopped }: { sentence: string; stopped: boolean }) => `${sentence}${stopped ? " · stopped" : ""}`,
|
|
109
112
|
workSteps: "Work steps",
|
|
110
113
|
streamLoginRequired: "Your session has ended. Sign in again to continue this chat.",
|
|
111
114
|
streamAccessDenied: "You no longer have access to this chat.",
|
|
@@ -290,6 +293,7 @@ const messages = i18n.define({
|
|
|
290
293
|
tableLinks: "Links",
|
|
291
294
|
tableOpen: "Öffnen",
|
|
292
295
|
tableEmpty: "Keine passenden Zeilen.",
|
|
296
|
+
chartUnavailable: "Dieses Diagramm kann nicht angezeigt werden.",
|
|
293
297
|
tableCapped:
|
|
294
298
|
"Die ersten 100 zurückgegebenen Zeilen werden angezeigt. Das vollständige Werkzeugergebnis bleibt in den Details verfügbar.",
|
|
295
299
|
tableMore: "Weitere Zeilen sind verfügbar. Frage nach der nächsten Seite oder öffne das Ergebnis in der Anwendung.",
|
|
@@ -372,6 +376,7 @@ const messages = i18n.define({
|
|
|
372
376
|
stepDeliveringFile: "Stellt eine Datei bereit",
|
|
373
377
|
stepPreparingView: "Bereitet eine Ansicht vor",
|
|
374
378
|
stepPreparingCard: "Bereitet eine Karte vor",
|
|
379
|
+
stepPreparingChart: "Bereitet ein Diagramm vor",
|
|
375
380
|
stepCompacting: "Fasst das Gespräch zusammen",
|
|
376
381
|
stepTool: "Nutzt ein Werkzeug",
|
|
377
382
|
stepFailed: "fehlgeschlagen",
|
|
@@ -386,7 +391,7 @@ const messages = i18n.define({
|
|
|
386
391
|
groupLoad: "Werkzeuge und Wissen geladen",
|
|
387
392
|
groupFailed: ({ count }) => `${count} fehlgeschlagen`,
|
|
388
393
|
groupRejected: ({ count }) => `${count} abgelehnt`,
|
|
389
|
-
receiptDone: ({ title }) =>
|
|
394
|
+
receiptDone: ({ title }) => `Erledigt: ${title}`,
|
|
390
395
|
receiptApproved: ({ title }) => `Freigegeben: ${title}`,
|
|
391
396
|
receiptRunning: ({ title }) => `Wird ausgeführt: ${title}`,
|
|
392
397
|
approvalNeeded: ({ title }) => `Freigabe nötig: ${title}`,
|
|
@@ -394,6 +399,7 @@ const messages = i18n.define({
|
|
|
394
399
|
receiptRejected: ({ title }) => `Abgelehnt: ${title}`,
|
|
395
400
|
receiptFailed: ({ title }) => `Fehlgeschlagen: ${title}`,
|
|
396
401
|
receiptNotRun: ({ title, stopped }) => `Nicht ausgeführt: ${title}${stopped ? " · gestoppt" : ""}`,
|
|
402
|
+
receiptNotRunSentence: ({ sentence, stopped }) => `${sentence}${stopped ? " · gestoppt" : ""}`,
|
|
397
403
|
workSteps: "Arbeitsschritte",
|
|
398
404
|
streamLoginRequired: "Deine Sitzung ist abgelaufen. Melde dich erneut an, um diesen Chat fortzusetzen.",
|
|
399
405
|
streamAccessDenied: "Du hast keinen Zugriff mehr auf diesen Chat.",
|
|
@@ -10,6 +10,7 @@ const toolNames = i18n.define({
|
|
|
10
10
|
en: {
|
|
11
11
|
calculate: "Calculate",
|
|
12
12
|
card: "Card",
|
|
13
|
+
chart: "Chart",
|
|
13
14
|
survey: "Survey",
|
|
14
15
|
text_editor: "Text editor",
|
|
15
16
|
local_bash: "Local Bash",
|
|
@@ -77,6 +78,7 @@ const toolNames = i18n.define({
|
|
|
77
78
|
de: {
|
|
78
79
|
calculate: "Rechnen",
|
|
79
80
|
card: "Karte",
|
|
81
|
+
chart: "Diagramm",
|
|
80
82
|
survey: "Umfrage",
|
|
81
83
|
text_editor: "Texteditor",
|
|
82
84
|
local_bash: "Lokale Bash",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AiTurnBlock, isRenderableTurnBlock } from "../protocol";
|
|
2
2
|
import type { AiStoredMessage } from "../types";
|
|
3
3
|
import { hasCapabilityTable } from "./capability-result";
|
|
4
|
-
import { isCardToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
4
|
+
import { isCardToolName, isChartToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
5
5
|
import { isFailedTool } from "./tool-groups";
|
|
6
6
|
|
|
7
7
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -69,7 +69,7 @@ const isResult = (block: ToolBlock, phase: AiTurnPhase, codePresentations: boole
|
|
|
69
69
|
// A running delivery reserves its place once its arguments arrived: a new version of an earlier result must find the
|
|
70
70
|
// earlier place before it takes one of its own.
|
|
71
71
|
const delivered = block.status === "completed" || (live(phase) && block.status === "running" && block.args !== undefined);
|
|
72
|
-
if (block.name === "present" || isCardToolName(block.name)) return delivered;
|
|
72
|
+
if (block.name === "present" || isCardToolName(block.name) || isChartToolName(block.name)) return delivered;
|
|
73
73
|
if (block.name === "code_present") return codePresentations && delivered;
|
|
74
74
|
return hasCapabilityTable(block);
|
|
75
75
|
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { markdown } from "../../shared";
|
|
18
18
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
19
|
import type { AiTurnError } from "../types";
|
|
20
|
+
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
20
21
|
import {
|
|
21
22
|
type AiApprovalDecision,
|
|
22
23
|
ApprovalBlockView,
|
|
@@ -26,9 +27,18 @@ import {
|
|
|
26
27
|
TextEditorToolView,
|
|
27
28
|
} from "./blocks";
|
|
28
29
|
import { CapabilityTablePreview } from "./capability-table";
|
|
30
|
+
import { CloudChartBlock } from "./chart-block";
|
|
29
31
|
import { PresentToolBlock } from "./file-tools";
|
|
30
32
|
import { useAiChatActions } from "./message-actions";
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
aiToolIcon,
|
|
35
|
+
capabilityErrorDescription,
|
|
36
|
+
displayToolName,
|
|
37
|
+
isCardToolName,
|
|
38
|
+
isChartToolName,
|
|
39
|
+
isRecord,
|
|
40
|
+
isSurveyToolName,
|
|
41
|
+
} from "./message-utils";
|
|
32
42
|
import { aiChatMessages } from "./messages";
|
|
33
43
|
import { AssistantMarkdownBlock } from "./primitives";
|
|
34
44
|
import { AiToolActivity, AiToolDisclosureProvider, type AiToolDisclosureState } from "./tool-disclosure";
|
|
@@ -133,6 +143,7 @@ const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean
|
|
|
133
143
|
if (name === "present") return withTarget(t.stepDeliveringFile);
|
|
134
144
|
if (name === "code_present") return withTarget(t.stepPreparingView);
|
|
135
145
|
if (isCardToolName(name)) return t.stepPreparingCard;
|
|
146
|
+
if (isChartToolName(name)) return t.stepPreparingChart;
|
|
136
147
|
if (["code_write", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
137
148
|
return withTarget(t.stepWritingCode);
|
|
138
149
|
if (["code_run", "code_action", "code_sql"].includes(name)) return withTarget(t.stepRunningCode);
|
|
@@ -436,6 +447,9 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
436
447
|
<Match when={isCardToolName(current().name)}>
|
|
437
448
|
<CloudCardBlock args={current().args} />
|
|
438
449
|
</Match>
|
|
450
|
+
<Match when={isChartToolName(current().name)}>
|
|
451
|
+
<CloudChartBlock args={current().args} completed={current().status === "completed"} />
|
|
452
|
+
</Match>
|
|
439
453
|
<Match when={true}>
|
|
440
454
|
<Show when={!props.receipt()}>
|
|
441
455
|
<CapabilityResultRow block={current()} />
|
|
@@ -455,33 +469,37 @@ type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
|
455
469
|
|
|
456
470
|
/**
|
|
457
471
|
* One line for an action in place 4, the same element while it runs and once it is done, rejected, failed, or not run.
|
|
458
|
-
* It
|
|
459
|
-
* application's own summary of
|
|
472
|
+
* It says what happened in the owning app's words: its sentence for the outcome, such as "Moved report.pdf to the
|
|
473
|
+
* trash", else, once done, the application's own summary of the call. Without either it puts the state in the reader's
|
|
474
|
+
* language before what the call does, such as "Rejected: Write report.md", or before its title and target.
|
|
460
475
|
*/
|
|
461
476
|
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
462
477
|
const locale = useLocale();
|
|
463
478
|
const t = () => aiChatMessages(locale());
|
|
464
479
|
const block = () => props.block;
|
|
465
480
|
const subject = () => {
|
|
481
|
+
const sentence = toolSubject(block(), locale());
|
|
482
|
+
if (sentence) return sentence;
|
|
466
483
|
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
467
484
|
const target = toolTarget(block());
|
|
468
485
|
return target ? `${title} · ${target}` : title;
|
|
469
486
|
};
|
|
487
|
+
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
470
488
|
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
471
489
|
const label = () => {
|
|
472
490
|
const state = props.state;
|
|
473
491
|
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
474
492
|
if (state === "done") {
|
|
475
|
-
const summary = resultSummary(block());
|
|
476
|
-
if (summary) return summary;
|
|
477
493
|
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
478
|
-
return
|
|
479
|
-
|
|
480
|
-
: t().receiptApproved({ title: subject() });
|
|
494
|
+
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
495
|
+
return toolOutcome(block(), "done", locale()) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
481
496
|
}
|
|
482
|
-
if (state === "rejected") return t().receiptRejected({ title: subject() });
|
|
497
|
+
if (state === "rejected") return toolOutcome(block(), "rejected", locale()) ?? t().receiptRejected({ title: subject() });
|
|
483
498
|
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
484
|
-
|
|
499
|
+
const notRun = toolOutcome(block(), "notRun", locale());
|
|
500
|
+
return notRun
|
|
501
|
+
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
502
|
+
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
485
503
|
};
|
|
486
504
|
const declined = () => props.state === "rejected" || props.state === "not_run";
|
|
487
505
|
return (
|
|
@@ -529,7 +547,7 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
529
547
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
530
548
|
announce(
|
|
531
549
|
aiChatMessages(locale()).approvalNeeded({
|
|
532
|
-
title: current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
550
|
+
title: toolSubject(current.block, locale()) ?? current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
533
551
|
}),
|
|
534
552
|
);
|
|
535
553
|
}),
|
|
@@ -3,8 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
4
4
|
import { getApp } from "../_internal/registry";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
|
+
import { IDENTITY_REFRESH_TIMEOUT_MS } from "../services/identity/constants";
|
|
6
7
|
import { signInvocationToken } from "../services/identity/invocation-token";
|
|
7
8
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
9
|
+
import { logger } from "../services/logging";
|
|
8
10
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
9
11
|
import { TIMEZONE_COOKIE } from "../shared/time";
|
|
10
12
|
import { CodeToolFailure } from "./browser-code-contracts";
|
|
@@ -13,6 +15,9 @@ import { CODE_CAPABILITY_TOKEN_HEADER, codeCapabilityOperation } from "./code-ca
|
|
|
13
15
|
import { authorizeCodeExecution } from "./code-execution";
|
|
14
16
|
import { aiConversations } from "./store";
|
|
15
17
|
|
|
18
|
+
const log = logger("ai:code-runtime");
|
|
19
|
+
const ISSUANCE_TIMEOUT_MS = 5_000;
|
|
20
|
+
|
|
16
21
|
const Reply = z.object({
|
|
17
22
|
ok: z.literal(true),
|
|
18
23
|
data: z.object({
|
|
@@ -49,54 +54,64 @@ export const runManagedCodeTool =
|
|
|
49
54
|
await context.reportProgress?.(context.locale?.startsWith("de") ? "Ausführungshost verbinden" : "Connecting execution host");
|
|
50
55
|
const app = await getApp("assistant");
|
|
51
56
|
if (!app) throw new Error("Assistant code host is unavailable");
|
|
57
|
+
const issue = async (audience: { targetAppId: string; callingAppId: string; operation: string }) => {
|
|
58
|
+
// A token is signed before its request goes out, so a timed-out issuance
|
|
59
|
+
// never reached the host and is safe to repeat. Retry long enough for a
|
|
60
|
+
// stalled shared refresh to finish or fall back to the cached key. Every
|
|
61
|
+
// timeout used up a full attempt or ended a shared load, so this cannot spin.
|
|
62
|
+
const giveUpAt = Date.now() + IDENTITY_REFRESH_TIMEOUT_MS + ISSUANCE_TIMEOUT_MS;
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
return await withActiveIdentitySigner(
|
|
66
|
+
"invocation",
|
|
67
|
+
(signer) =>
|
|
68
|
+
signInvocationToken({
|
|
69
|
+
...audience,
|
|
70
|
+
schemaHash: null,
|
|
71
|
+
authority: {
|
|
72
|
+
sub: actor.user.id,
|
|
73
|
+
principal_type: "user",
|
|
74
|
+
access_subject_type: "user",
|
|
75
|
+
access_subject_id: actor.user.id,
|
|
76
|
+
credential_kind: "session",
|
|
77
|
+
scopes: [],
|
|
78
|
+
},
|
|
79
|
+
signer,
|
|
80
|
+
issuer: signer.issuer,
|
|
81
|
+
}),
|
|
82
|
+
{ signal: context.signal, timeoutMs: ISSUANCE_TIMEOUT_MS },
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (context.signal.aborted || !(error instanceof DOMException && error.name === "TimeoutError")) throw error;
|
|
86
|
+
if (Date.now() < giveUpAt) continue;
|
|
87
|
+
log.warn("Code call gave up on identity issuance", { tool: name, error: error.message });
|
|
88
|
+
throw new Error(
|
|
89
|
+
context.locale?.startsWith("de")
|
|
90
|
+
? "Der Identitätsdienst hat nicht rechtzeitig geantwortet. Prüfe, ob dieser Aufruf schon gestartet ist, bevor du ihn erneut ausführst."
|
|
91
|
+
: "The identity service did not respond in time. Check whether this call already started before running it again.",
|
|
92
|
+
{ cause: error },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Polls run every 250 ms for as long as the code runs. Reuse both tokens
|
|
98
|
+
// while they stay valid for at least 10 s, so a long run or an approval
|
|
99
|
+
// round trip does not depend on a fresh issuance for every poll.
|
|
100
|
+
const usable = (token: Awaited<ReturnType<typeof signInvocationToken>> | undefined) =>
|
|
101
|
+
token && token.claims.exp * 1000 - Date.now() >= 10_000 ? token : undefined;
|
|
102
|
+
let signed: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
52
103
|
let callback: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
53
104
|
const request = async (decision?: { id: string; approved: boolean }) => {
|
|
54
105
|
context.signal.throwIfAborted();
|
|
55
106
|
await authorizeCodeExecution(context.conversationId!, context.turnId!, actor.user.id);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
authority: {
|
|
65
|
-
sub: actor.user.id,
|
|
66
|
-
principal_type: "user",
|
|
67
|
-
access_subject_type: "user",
|
|
68
|
-
access_subject_id: actor.user.id,
|
|
69
|
-
credential_kind: "session",
|
|
70
|
-
scopes: [],
|
|
71
|
-
},
|
|
72
|
-
signer,
|
|
73
|
-
issuer: signer.issuer,
|
|
74
|
-
}),
|
|
75
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
76
|
-
);
|
|
77
|
-
if (!callback || callback.claims.exp * 1000 - Date.now() < 10_000) {
|
|
78
|
-
callback = await withActiveIdentitySigner(
|
|
79
|
-
"invocation",
|
|
80
|
-
(signer) =>
|
|
81
|
-
signInvocationToken({
|
|
82
|
-
targetAppId: "core",
|
|
83
|
-
callingAppId: "assistant",
|
|
84
|
-
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
85
|
-
schemaHash: null,
|
|
86
|
-
authority: {
|
|
87
|
-
sub: actor.user.id,
|
|
88
|
-
principal_type: "user",
|
|
89
|
-
access_subject_type: "user",
|
|
90
|
-
access_subject_id: actor.user.id,
|
|
91
|
-
credential_kind: "session",
|
|
92
|
-
scopes: [],
|
|
93
|
-
},
|
|
94
|
-
signer,
|
|
95
|
-
issuer: signer.issuer,
|
|
96
|
-
}),
|
|
97
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
107
|
+
signed = usable(signed) ?? (await issue({ targetAppId: "assistant", callingAppId: "core", operation: `tool:${name}` }));
|
|
108
|
+
callback =
|
|
109
|
+
usable(callback) ??
|
|
110
|
+
(await issue({
|
|
111
|
+
targetAppId: "core",
|
|
112
|
+
callingAppId: "assistant",
|
|
113
|
+
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
114
|
+
}));
|
|
100
115
|
const headers = new Headers({ authorization: `Bearer ${signed.token}`, "content-type": "application/json" });
|
|
101
116
|
headers.set(CODE_CAPABILITY_TOKEN_HEADER, callback.token);
|
|
102
117
|
if (context.locale) headers.set(LOCALE_HEADER, context.locale);
|