@k2b/cloud 0.29.1 → 0.31.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 +4 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +113 -5
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +24 -5
- package/src/_internal/registry-validation.ts +5 -0
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -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/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- 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 +97 -40
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +26 -2
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +6 -2
- package/src/ai/chat/turn-layout.ts +10 -2
- package/src/ai/chat/turn-view.tsx +123 -36
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +109 -61
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +189 -35
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +17 -1
- package/src/ai/migrate.ts +172 -14
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/runtime.ts +46 -7
- 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 +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +47 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/capabilities.ts +3 -2
- 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 +291 -101
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- 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/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +30 -0
- package/src/capabilities/executions.ts +3 -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/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/app.ts +34 -2
- 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 +10 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +67 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/accounts/users.ts +15 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -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/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- 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/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- 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/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
- 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,
|
|
@@ -30,7 +34,7 @@ import { AssistantMarkdownBlock } from "./primitives";
|
|
|
30
34
|
import { AiToolActivity } from "./tool-disclosure";
|
|
31
35
|
import { isFailedTool } from "./tool-groups";
|
|
32
36
|
import { CloudCardBlock, CloudSurveyBlock, CloudSurveyResultBlock, CloudTextEditorBlock, CloudTextEditorResultBlock } from "./visual-tools";
|
|
33
|
-
import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock } from "./web-tools";
|
|
37
|
+
import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock, WebsiteReceipts, websiteReceipts } from "./web-tools";
|
|
34
38
|
|
|
35
39
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
36
40
|
type ReviewDetail = NonNullable<CapabilityActionReview["details"]>[number];
|
|
@@ -201,7 +205,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
201
205
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
|
202
206
|
// The call this card answered: a later approval for the same block may already have arrived when the server accepts.
|
|
203
207
|
let decision: AiApprovalDecision | null = null;
|
|
204
|
-
const approval = mutation.create<void, { approved: boolean; remember?: "always" }>({
|
|
208
|
+
const approval = mutation.create<void, { approved: boolean; remember?: "chat" | "always" }>({
|
|
205
209
|
mutation: async (input) => {
|
|
206
210
|
if (!actions.onApproval) throw new Error("Approval is unavailable.");
|
|
207
211
|
await actions.onApproval(request(), input);
|
|
@@ -211,16 +215,22 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
211
215
|
if (decision) props.onDecided?.(decision);
|
|
212
216
|
},
|
|
213
217
|
});
|
|
214
|
-
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
218
|
+
const submit = (input: { approved: boolean; remember?: "chat" | "always" }) => {
|
|
215
219
|
if (actionDisabled() || approval.loading()) return;
|
|
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>}>
|
|
@@ -393,6 +411,16 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
393
411
|
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
394
412
|
action: () => setDetailsOpen((open) => !open),
|
|
395
413
|
},
|
|
414
|
+
// The narrower chat reach comes first: it ends with this chat.
|
|
415
|
+
...(props.block.approval?.allowChat
|
|
416
|
+
? [
|
|
417
|
+
{
|
|
418
|
+
label: props.block.approval.website ? t().allowWebsiteForChat : t().chatApprove,
|
|
419
|
+
icon: props.block.approval.website ? "ti ti-world-check" : "ti ti-message-check",
|
|
420
|
+
action: () => submit({ approved: true, remember: "chat" }),
|
|
421
|
+
},
|
|
422
|
+
]
|
|
423
|
+
: []),
|
|
396
424
|
...(props.block.approval?.allowAlways
|
|
397
425
|
? [
|
|
398
426
|
{
|
|
@@ -518,11 +546,11 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
518
546
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
519
547
|
const locale = useLocale();
|
|
520
548
|
const presentation = () => props.block.presentation;
|
|
521
|
-
const title = () => presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
549
|
+
const title = () => toolSubject(props.block, locale()) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
522
550
|
return (
|
|
523
551
|
<Chat.Activity
|
|
524
552
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
525
|
-
label={aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
553
|
+
label={toolOutcome(props.block, "rejected", locale()) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
526
554
|
accent={presentation()?.appAccent}
|
|
527
555
|
/>
|
|
528
556
|
);
|
|
@@ -694,6 +722,9 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
694
722
|
<Match when={isCardToolName(props.block.name) && !props.block.isError}>
|
|
695
723
|
<CloudCardBlock args={props.block.args} />
|
|
696
724
|
</Match>
|
|
725
|
+
<Match when={isChartToolName(props.block.name) && !props.block.isError}>
|
|
726
|
+
<CloudChartBlock args={props.block.args} completed={status() === "completed"} />
|
|
727
|
+
</Match>
|
|
697
728
|
<Match when={isSurveyToolName(props.block.name) && !props.block.isError}>
|
|
698
729
|
<SurveyToolView turnId={props.turnId} block={props.block} active={props.active} />
|
|
699
730
|
</Match>
|
|
@@ -704,6 +735,9 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
704
735
|
<Chat.Activity label={displayToolName(props.block.name, locale())} icon={aiToolIcon(props.block.name)} busy />
|
|
705
736
|
</Match>
|
|
706
737
|
</Switch>
|
|
738
|
+
<Show when={websiteReceipts(props.block).length > 0}>
|
|
739
|
+
<WebsiteReceipts block={props.block} />
|
|
740
|
+
</Show>
|
|
707
741
|
<Show when={hasCapabilityTable(props.block)}>
|
|
708
742
|
<CapabilityTablePreview
|
|
709
743
|
result={props.block.result}
|
|
@@ -791,7 +825,22 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
791
825
|
const locale = useLocale();
|
|
792
826
|
const t = () => aiChatMessages(locale());
|
|
793
827
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
794
|
-
|
|
828
|
+
/**
|
|
829
|
+
* A call that acts on something reads as its sentence, which already names what it acts on: what it will do while
|
|
830
|
+
* it runs or waits, what it did once done, and what did not happen when it was rejected.
|
|
831
|
+
*/
|
|
832
|
+
const sentence = () => {
|
|
833
|
+
const status = props.block.status;
|
|
834
|
+
const outcome =
|
|
835
|
+
status === "completed" && !props.block.isError
|
|
836
|
+
? toolOutcome(props.block, "done", locale())
|
|
837
|
+
: status === "rejected"
|
|
838
|
+
? toolOutcome(props.block, "rejected", locale())
|
|
839
|
+
: null;
|
|
840
|
+
return outcome ?? toolSubject(props.block, locale());
|
|
841
|
+
};
|
|
842
|
+
const detail = () =>
|
|
843
|
+
sentence() ? undefined : [args().path, args().name, args().query, args().title].find((value) => typeof value === "string");
|
|
795
844
|
const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
|
|
796
845
|
const state = () =>
|
|
797
846
|
waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
|
|
@@ -817,35 +866,43 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
817
866
|
return aiToolIcon(props.block.name, props.block.presentation?.appIcon);
|
|
818
867
|
};
|
|
819
868
|
return (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
<div
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
869
|
+
<>
|
|
870
|
+
<AiToolActivity
|
|
871
|
+
blockId={props.block.id}
|
|
872
|
+
label={sentence() ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
873
|
+
description={[
|
|
874
|
+
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
875
|
+
typeof detail() === "string" ? String(detail()) : "",
|
|
876
|
+
state(),
|
|
877
|
+
outcome(),
|
|
878
|
+
]
|
|
879
|
+
.filter(Boolean)
|
|
880
|
+
.join(" · ")}
|
|
881
|
+
icon={icon()}
|
|
882
|
+
busy={props.busy && props.block.status === "running"}
|
|
883
|
+
bodyInset={false}
|
|
884
|
+
renderBody={() => (
|
|
885
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
886
|
+
<div
|
|
887
|
+
class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
|
|
888
|
+
tabIndex={0}
|
|
889
|
+
role="region"
|
|
890
|
+
aria-label={t().toolInputOutput}
|
|
891
|
+
>
|
|
892
|
+
<ToolDetail title={t().input} toolName={props.block.name} value={props.block.args} />
|
|
893
|
+
<ToolDetail title={t().output} toolName={props.block.name} value={props.block.result} />
|
|
894
|
+
</div>
|
|
895
|
+
{/* Below the input and output, so an image that arrives in an open step grows in like its output and moves nothing above. */}
|
|
896
|
+
<Show when={image()}>
|
|
897
|
+
{(viewed) => <StepImage path={viewed().path} src={viewed().src} description={viewed().description} />}
|
|
898
|
+
</Show>
|
|
844
899
|
</div>
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
)}
|
|
849
|
-
|
|
900
|
+
)}
|
|
901
|
+
/>
|
|
902
|
+
{/* Each request a website approval let through is a step of its own, with its full URL. */}
|
|
903
|
+
<Show when={websiteReceipts(props.block).length > 0}>
|
|
904
|
+
<WebsiteReceipts block={props.block} />
|
|
905
|
+
</Show>
|
|
906
|
+
</>
|
|
850
907
|
);
|
|
851
908
|
}
|
|
@@ -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
|
+
}
|
|
@@ -12,7 +12,7 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
12
12
|
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
13
|
* `window.reconnect(on)` sets whether the stream reconnects,
|
|
14
14
|
* `window.continued` lists the messages that **Continue** sent,
|
|
15
|
-
* `window.approvals` lists the approval decisions, and
|
|
15
|
+
* `window.approvals` lists the approval decisions, `window.revoked` the websites a receipt revoked, and
|
|
16
16
|
* `window.opened` lists the chat files the timeline opened. Chat files load from `/files?path=`.
|
|
17
17
|
*/
|
|
18
18
|
const [state, setState] = createStore(emptyProjection());
|
|
@@ -20,9 +20,11 @@ const [reconnecting, setReconnecting] = createSignal(false);
|
|
|
20
20
|
const continued: string[] = [];
|
|
21
21
|
const opened: string[] = [];
|
|
22
22
|
const approvals: unknown[] = [];
|
|
23
|
+
const revoked: string[] = [];
|
|
23
24
|
Object.assign(window, {
|
|
24
25
|
continued,
|
|
25
26
|
approvals,
|
|
27
|
+
revoked,
|
|
26
28
|
opened,
|
|
27
29
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
28
30
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
@@ -42,6 +44,7 @@ render(
|
|
|
42
44
|
actions={{
|
|
43
45
|
onContinueTurn: (message) => void continued.push(message),
|
|
44
46
|
onApproval: async (request, input) => void approvals.push({ callId: request.callId, ...input }),
|
|
47
|
+
onRevokeWebsite: async (origin) => void revoked.push(origin),
|
|
45
48
|
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
46
49
|
onOpenFile: (path) => void opened.push(path),
|
|
47
50
|
}}
|
|
@@ -26,7 +26,7 @@ import { aiChatMessages } from "./messages";
|
|
|
26
26
|
/** The active-turn coordinates an approval/tool action needs to resolve on the server. */
|
|
27
27
|
export type AiTurnActionRequest = { turnId: string; callId: string; name: string };
|
|
28
28
|
|
|
29
|
-
type ApprovalHandler = (request: AiTurnActionRequest, input: { approved: boolean; remember?: "always" }) => void | Promise<void>;
|
|
29
|
+
type ApprovalHandler = (request: AiTurnActionRequest, input: { approved: boolean; remember?: "chat" | "always" }) => void | Promise<void>;
|
|
30
30
|
type FrontendToolResultHandler = (request: AiTurnActionRequest, result: unknown) => void | Promise<void>;
|
|
31
31
|
type ForkMessageHandler = (entry: AiStoredMessage, input?: AiForkMessageInput) => void | Promise<void>;
|
|
32
32
|
type RetryMessageHandler = (entry: AiStoredMessage, input?: AiRetryMessageInput) => void | Promise<void>;
|
|
@@ -42,6 +42,8 @@ export type AiChatActions = {
|
|
|
42
42
|
/** Prevents turn-continuation actions while the current turn is stopping. */
|
|
43
43
|
actionDisabled?: () => boolean;
|
|
44
44
|
onApproval?: ApprovalHandler;
|
|
45
|
+
/** Revoke the approval that lets this chat read `origin` without asking; the next request asks again. */
|
|
46
|
+
onRevokeWebsite?: (origin: string) => Promise<void>;
|
|
45
47
|
onFrontendToolResult?: FrontendToolResultHandler;
|
|
46
48
|
onForkMessage?: ForkMessageHandler;
|
|
47
49
|
onRetryMessage?: RetryMessageHandler;
|
|
@@ -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",
|
|
@@ -96,9 +98,10 @@ const messages = i18n.define({
|
|
|
96
98
|
openImage: ({ name }: { name: string }) => `Open image ${name}`,
|
|
97
99
|
steered: "Conversation steered",
|
|
98
100
|
groupLoad: "Loaded tools and guidance",
|
|
101
|
+
repeatCount: ({ count }: { count: number }) => `${count} times`,
|
|
99
102
|
groupFailed: ({ count }: { count: number }) => `${count} failed`,
|
|
100
103
|
groupRejected: ({ count }: { count: number }) => `${count} rejected`,
|
|
101
|
-
receiptDone: ({ title }: { title: string }) =>
|
|
104
|
+
receiptDone: ({ title }: { title: string }) => `Done: ${title}`,
|
|
102
105
|
receiptApproved: ({ title }: { title: string }) => `Approved: ${title}`,
|
|
103
106
|
receiptRunning: ({ title }: { title: string }) => `Running: ${title}`,
|
|
104
107
|
approvalNeeded: ({ title }: { title: string }) => `Approval needed: ${title}`,
|
|
@@ -106,6 +109,7 @@ const messages = i18n.define({
|
|
|
106
109
|
receiptRejected: ({ title }: { title: string }) => `Rejected: ${title}`,
|
|
107
110
|
receiptFailed: ({ title }: { title: string }) => `Failed: ${title}`,
|
|
108
111
|
receiptNotRun: ({ title, stopped }: { title: string; stopped: boolean }) => `Not run: ${title}${stopped ? " · stopped" : ""}`,
|
|
112
|
+
receiptNotRunSentence: ({ sentence, stopped }: { sentence: string; stopped: boolean }) => `${sentence}${stopped ? " · stopped" : ""}`,
|
|
109
113
|
workSteps: "Work steps",
|
|
110
114
|
streamLoginRequired: "Your session has ended. Sign in again to continue this chat.",
|
|
111
115
|
streamAccessDenied: "You no longer have access to this chat.",
|
|
@@ -117,6 +121,14 @@ const messages = i18n.define({
|
|
|
117
121
|
approved: "Approved",
|
|
118
122
|
rejected: "Rejected",
|
|
119
123
|
alwaysApprove: "Always approve",
|
|
124
|
+
chatApprove: "Approve for this chat",
|
|
125
|
+
allowWebsiteForChat: "Allow this website for this chat",
|
|
126
|
+
websiteAllowedForChat: ({ count }: { count: number }) =>
|
|
127
|
+
`Allowed for this chat · ${count === 1 ? "1 request without asking" : `${count} requests without asking`}`,
|
|
128
|
+
revoke: "Revoke",
|
|
129
|
+
revokeFailed: "Could not revoke. Try again.",
|
|
130
|
+
revokeWebsiteFor: ({ host }: { host: string }) => `Revoke the approval for ${host}`,
|
|
131
|
+
websiteRevoked: "Revoked. The next request asks again.",
|
|
120
132
|
details: "Details",
|
|
121
133
|
hideDetails: "Hide details",
|
|
122
134
|
moreOptions: ({ title }: { title: string }) => `More options for ${title}`,
|
|
@@ -290,6 +302,7 @@ const messages = i18n.define({
|
|
|
290
302
|
tableLinks: "Links",
|
|
291
303
|
tableOpen: "Öffnen",
|
|
292
304
|
tableEmpty: "Keine passenden Zeilen.",
|
|
305
|
+
chartUnavailable: "Dieses Diagramm kann nicht angezeigt werden.",
|
|
293
306
|
tableCapped:
|
|
294
307
|
"Die ersten 100 zurückgegebenen Zeilen werden angezeigt. Das vollständige Werkzeugergebnis bleibt in den Details verfügbar.",
|
|
295
308
|
tableMore: "Weitere Zeilen sind verfügbar. Frage nach der nächsten Seite oder öffne das Ergebnis in der Anwendung.",
|
|
@@ -372,6 +385,7 @@ const messages = i18n.define({
|
|
|
372
385
|
stepDeliveringFile: "Stellt eine Datei bereit",
|
|
373
386
|
stepPreparingView: "Bereitet eine Ansicht vor",
|
|
374
387
|
stepPreparingCard: "Bereitet eine Karte vor",
|
|
388
|
+
stepPreparingChart: "Bereitet ein Diagramm vor",
|
|
375
389
|
stepCompacting: "Fasst das Gespräch zusammen",
|
|
376
390
|
stepTool: "Nutzt ein Werkzeug",
|
|
377
391
|
stepFailed: "fehlgeschlagen",
|
|
@@ -384,9 +398,10 @@ const messages = i18n.define({
|
|
|
384
398
|
openImage: ({ name }) => `Bild ${name} öffnen`,
|
|
385
399
|
steered: "Richtung geändert",
|
|
386
400
|
groupLoad: "Werkzeuge und Wissen geladen",
|
|
401
|
+
repeatCount: ({ count }) => `${count}-mal`,
|
|
387
402
|
groupFailed: ({ count }) => `${count} fehlgeschlagen`,
|
|
388
403
|
groupRejected: ({ count }) => `${count} abgelehnt`,
|
|
389
|
-
receiptDone: ({ title }) =>
|
|
404
|
+
receiptDone: ({ title }) => `Erledigt: ${title}`,
|
|
390
405
|
receiptApproved: ({ title }) => `Freigegeben: ${title}`,
|
|
391
406
|
receiptRunning: ({ title }) => `Wird ausgeführt: ${title}`,
|
|
392
407
|
approvalNeeded: ({ title }) => `Freigabe nötig: ${title}`,
|
|
@@ -394,6 +409,7 @@ const messages = i18n.define({
|
|
|
394
409
|
receiptRejected: ({ title }) => `Abgelehnt: ${title}`,
|
|
395
410
|
receiptFailed: ({ title }) => `Fehlgeschlagen: ${title}`,
|
|
396
411
|
receiptNotRun: ({ title, stopped }) => `Nicht ausgeführt: ${title}${stopped ? " · gestoppt" : ""}`,
|
|
412
|
+
receiptNotRunSentence: ({ sentence, stopped }) => `${sentence}${stopped ? " · gestoppt" : ""}`,
|
|
397
413
|
workSteps: "Arbeitsschritte",
|
|
398
414
|
streamLoginRequired: "Deine Sitzung ist abgelaufen. Melde dich erneut an, um diesen Chat fortzusetzen.",
|
|
399
415
|
streamAccessDenied: "Du hast keinen Zugriff mehr auf diesen Chat.",
|
|
@@ -405,6 +421,14 @@ const messages = i18n.define({
|
|
|
405
421
|
approved: "Freigegeben",
|
|
406
422
|
rejected: "Abgelehnt",
|
|
407
423
|
alwaysApprove: "Immer freigeben",
|
|
424
|
+
chatApprove: "Für diesen Chat freigeben",
|
|
425
|
+
allowWebsiteForChat: "Diese Website für diesen Chat erlauben",
|
|
426
|
+
websiteAllowedForChat: ({ count }: { count: number }) =>
|
|
427
|
+
`Für diesen Chat erlaubt · ${count === 1 ? "1 Abruf ohne Rückfrage" : `${count} Abrufe ohne Rückfrage`}`,
|
|
428
|
+
revoke: "Entziehen",
|
|
429
|
+
revokeFailed: "Konnte nicht entzogen werden. Versuche es erneut.",
|
|
430
|
+
revokeWebsiteFor: ({ host }: { host: string }) => `Freigabe für ${host} entziehen`,
|
|
431
|
+
websiteRevoked: "Entzogen. Der nächste Abruf fragt wieder.",
|
|
408
432
|
details: "Details",
|
|
409
433
|
hideDetails: "Details ausblenden",
|
|
410
434
|
moreOptions: ({ title }) => `Weitere Optionen für ${title}`,
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { aiSkillFilePathFromMount } from "../file-mount";
|
|
2
2
|
import type { AiTurnBlock } from "../protocol";
|
|
3
|
+
import { isRecord } from "./message-utils";
|
|
3
4
|
import { aiChatMessages } from "./messages";
|
|
4
5
|
|
|
5
6
|
type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
6
7
|
type Text = Extract<AiTurnBlock, { kind: "text" }>;
|
|
7
8
|
export type AiWorkEntry = Exclude<AiTurnBlock, { kind: "text" } | { kind: "steer_message" }>;
|
|
8
9
|
/**
|
|
9
|
-
* One row of the expanded work
|
|
10
|
-
*
|
|
10
|
+
* One row of the expanded work: an intermediate text, a step, or a group that folds housekeeping or repeated calls
|
|
11
|
+
* on one target and opens in place.
|
|
11
12
|
*/
|
|
12
13
|
export type AiWorkItem =
|
|
13
14
|
| { kind: "text"; block: Text }
|
|
14
15
|
| { kind: "step"; entry: AiWorkEntry }
|
|
15
|
-
| { kind: "housekeeping"; id: string; entries: AiWorkEntry[] }
|
|
16
|
+
| { kind: "housekeeping"; id: string; entries: AiWorkEntry[] }
|
|
17
|
+
| { kind: "repeat"; id: string; target: string; entries: AiWorkEntry[] };
|
|
16
18
|
|
|
17
19
|
/** A failed call, including a code run that reported an error. A rejected approval is a decision, not a failure. */
|
|
18
20
|
export function isFailedTool(tool: Tool): boolean {
|
|
@@ -50,33 +52,85 @@ export const isHousekeepingTool = (tool: Tool): boolean => {
|
|
|
50
52
|
|
|
51
53
|
const isStep = (entry: AiWorkEntry) => entry.kind === "tool" || entry.kind === "compaction";
|
|
52
54
|
|
|
55
|
+
/** The file, address, or query a folded row shows. A call without one never folds with others. */
|
|
56
|
+
const callTarget = (tool: Tool): string | undefined => {
|
|
57
|
+
const args = isRecord(tool.args) ? tool.args : {};
|
|
58
|
+
return [args.path, args.url, args.query].find((value): value is string => typeof value === "string" && value.trim() !== "")?.trim();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Arguments that page through one call's result, such as read_file's byte slices or an app's cursor and page size. */
|
|
62
|
+
const PAGING_ARGUMENTS = new Set(["offset", "length", "limit", "cursor", "after", "page", "pageSize"]);
|
|
63
|
+
|
|
64
|
+
type Fold = { kind: "housekeeping" } | { kind: "repeat"; name: string; target: string; identity: string };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a step folds with: housekeeping with housekeeping, a running or finished call with the same call apart from
|
|
68
|
+
* paging, such as reading one file in parts. Repeated calls need a target; failed, rejected, and waiting calls never repeat.
|
|
69
|
+
*/
|
|
70
|
+
const foldOf = (tool: Tool): Fold | null => {
|
|
71
|
+
if (isHousekeepingTool(tool)) return { kind: "housekeeping" };
|
|
72
|
+
const target = callTarget(tool);
|
|
73
|
+
if (!target || isFailedTool(tool) || (tool.status !== "completed" && tool.status !== "running")) return null;
|
|
74
|
+
const args = isRecord(tool.args) ? tool.args : {};
|
|
75
|
+
const identity = JSON.stringify(
|
|
76
|
+
Object.fromEntries(Object.entries(args).filter(([key]) => !PAGING_ARGUMENTS.has(key))),
|
|
77
|
+
(_key: string, value: unknown) =>
|
|
78
|
+
typeof value === "string"
|
|
79
|
+
? value.trim()
|
|
80
|
+
: isRecord(value)
|
|
81
|
+
? Object.fromEntries(
|
|
82
|
+
Object.keys(value)
|
|
83
|
+
.sort()
|
|
84
|
+
.map((key) => [key, value[key]]),
|
|
85
|
+
)
|
|
86
|
+
: value,
|
|
87
|
+
);
|
|
88
|
+
return { kind: "repeat", name: tool.name, target, identity };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const sameFold = (a: Fold, b: Fold): boolean =>
|
|
92
|
+
a.kind === "housekeeping" ? b.kind === "housekeeping" : b.kind === "repeat" && a.name === b.name && a.identity === b.identity;
|
|
93
|
+
|
|
53
94
|
/**
|
|
54
|
-
* The rows of the expanded work, in the original order. A run of at least two
|
|
55
|
-
* between them,
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
95
|
+
* The rows of the expanded work, in the original order. A run of at least two steps that fold together, with the
|
|
96
|
+
* reasoning between them, becomes one row: housekeeping, or consecutive instances of the same call apart from paging,
|
|
97
|
+
* such as reading one file in parts. Anything else between them, such as an intermediate text, ends the run. A run that
|
|
98
|
+
* would hold all but one of the work's steps shows its steps directly, so the work line never opens to a summary of
|
|
99
|
+
* itself. Entries listed in `shown` already show as rows of their own and never fold, so nothing a reader sees folds
|
|
100
|
+
* away under them.
|
|
101
|
+
* A folded row is keyed by its first tool, whose id is the same live and in history.
|
|
59
102
|
*/
|
|
60
103
|
export function groupWorkBlocks(blocks: readonly AiTurnBlock[], shown: ReadonlySet<string> = new Set()): AiWorkItem[] {
|
|
61
104
|
const entries = blocks.filter((block): block is AiWorkEntry | Text => block.kind !== "steer_message");
|
|
62
105
|
const steps = entries.filter((entry) => entry.kind !== "text" && isStep(entry)).length;
|
|
63
106
|
const items: AiWorkItem[] = [];
|
|
107
|
+
let fold: Fold | null = null;
|
|
64
108
|
let run: AiWorkEntry[] = [];
|
|
65
109
|
let trailing: AiWorkEntry[] = [];
|
|
66
110
|
const flush = () => {
|
|
67
|
-
if (run.length > 0) {
|
|
111
|
+
if (fold && run.length > 0) {
|
|
68
112
|
const runSteps = run.filter(isStep).length;
|
|
69
|
-
|
|
113
|
+
const id = run[0]!.id;
|
|
114
|
+
if (runSteps >= 2 && steps - runSteps >= 2)
|
|
115
|
+
items.push(
|
|
116
|
+
fold.kind === "housekeeping"
|
|
117
|
+
? { kind: "housekeeping", id, entries: run }
|
|
118
|
+
: { kind: "repeat", id, target: fold.target, entries: run },
|
|
119
|
+
);
|
|
70
120
|
else for (const entry of run) items.push({ kind: "step", entry });
|
|
71
121
|
}
|
|
72
|
-
// Reasoning after the last
|
|
122
|
+
// Reasoning after the last folded step belongs to what comes next.
|
|
73
123
|
for (const entry of trailing) items.push({ kind: "step", entry });
|
|
124
|
+
fold = null;
|
|
74
125
|
run = [];
|
|
75
126
|
trailing = [];
|
|
76
127
|
};
|
|
77
128
|
for (const entry of entries) {
|
|
78
129
|
const foldable = !shown.has(entry.id);
|
|
79
|
-
|
|
130
|
+
const next = foldable && entry.kind === "tool" ? foldOf(entry) : null;
|
|
131
|
+
if (next && entry.kind === "tool") {
|
|
132
|
+
if (fold && !sameFold(fold, next)) flush();
|
|
133
|
+
fold = next;
|
|
80
134
|
run.push(...trailing, entry);
|
|
81
135
|
trailing = [];
|
|
82
136
|
} else if (foldable && entry.kind === "thinking" && run.length > 0) trailing.push(entry);
|
|
@@ -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",
|
|
@@ -71,12 +72,14 @@ const toolNames = i18n.define({
|
|
|
71
72
|
code_restore: "Restore app version",
|
|
72
73
|
code_update: "Update app details",
|
|
73
74
|
code_create: "Create app",
|
|
74
|
-
code_write: "Save app
|
|
75
|
+
code_write: "Save app file",
|
|
76
|
+
code_import: "Import app files",
|
|
75
77
|
code_remove: "Remove app file",
|
|
76
78
|
},
|
|
77
79
|
de: {
|
|
78
80
|
calculate: "Rechnen",
|
|
79
81
|
card: "Karte",
|
|
82
|
+
chart: "Diagramm",
|
|
80
83
|
survey: "Umfrage",
|
|
81
84
|
text_editor: "Texteditor",
|
|
82
85
|
local_bash: "Lokale Bash",
|
|
@@ -138,7 +141,8 @@ const toolNames = i18n.define({
|
|
|
138
141
|
code_restore: "App-Version wiederherstellen",
|
|
139
142
|
code_update: "App-Details ändern",
|
|
140
143
|
code_create: "App erstellen",
|
|
141
|
-
code_write: "App-
|
|
144
|
+
code_write: "App-Datei speichern",
|
|
145
|
+
code_import: "App-Dateien importieren",
|
|
142
146
|
code_remove: "App-Datei entfernen",
|
|
143
147
|
},
|
|
144
148
|
},
|