@k2b/cloud 0.27.0 → 0.29.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 +7 -5
- package/src/_internal/cli-plugins.ts +18 -0
- package/src/_internal/define-app.ts +21 -5
- package/src/_internal/registry-validation.ts +30 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.tsx +11 -4
- package/src/ai/assistant-models.ts +25 -4
- package/src/ai/audio-tool.ts +3 -10
- package/src/ai/browser-code-contracts.ts +33 -63
- package/src/ai/browser.ts +1 -0
- package/src/ai/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +113 -24
- package/src/ai/chat/builtin-tools.tsx +3 -1
- package/src/ai/chat/composer-adapter.ts +8 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -3
- package/src/ai/chat/message-actions.tsx +124 -102
- package/src/ai/chat/message-utils.ts +57 -6
- package/src/ai/chat/messages.ts +214 -0
- package/src/ai/chat/presentation.tsx +67 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-layout.ts +12 -0
- package/src/ai/chat/turn-view.tsx +69 -10
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/client/controller.ts +23 -6
- package/src/ai/code-mode-skill.ts +21 -25
- package/src/ai/code-runtime-tools.ts +5 -1
- package/src/ai/code-source-contracts.ts +2 -2
- package/src/ai/compaction.ts +22 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +23 -17
- package/src/ai/executor.ts +262 -99
- package/src/ai/file-media-type.ts +4 -0
- package/src/ai/files-store.ts +17 -10
- package/src/ai/http.ts +11 -10
- package/src/ai/index.ts +3 -0
- package/src/ai/memories.ts +2 -0
- package/src/ai/memory-learning.ts +59 -5
- package/src/ai/memory-workflow-evidence.ts +35 -10
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +34 -1
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +26 -10
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +8 -0
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +14 -6
- package/src/ai/store.ts +102 -34
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +9 -22
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +104 -0
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/turn-policy.ts +2 -1
- package/src/ai/types.ts +29 -1
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/admin-outgoing-mail.ts +185 -5
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +82 -295
- package/src/browser/FileSaver.tsx +407 -0
- package/src/browser/app-badges.ts +16 -0
- package/src/browser/choose-files.browser-harness.tsx +22 -6
- package/src/browser/choose-files.tsx +23 -36
- package/src/browser/file-chooser-messages.ts +66 -0
- package/src/browser/file-providers.ts +258 -8
- package/src/browser/files.tsx +26 -3
- package/src/browser/notifications.ts +37 -1
- package/src/browser/provider-folder.tsx +301 -0
- package/src/browser/provider-list.ts +39 -0
- package/src/browser/save-files.browser-harness.tsx +35 -0
- package/src/browser/save-files.tsx +107 -0
- package/src/cli/admin/index.ts +3 -1
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +104 -7
- package/src/contracts/app.ts +10 -0
- package/src/contracts/commands.ts +3 -1
- package/src/contracts/notification-group.ts +6 -0
- package/src/contracts/notification-types.ts +6 -0
- package/src/contracts/outgoing-mail.ts +189 -1
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/user-notifications.ts +74 -0
- package/src/server/middleware/request-logger.ts +11 -2
- package/src/services/accounts/avatar.ts +5 -2
- package/src/services/help/store.ts +2 -1
- package/src/services/image-metadata.ts +438 -0
- package/src/services/index.ts +12 -1
- package/src/services/logging/redaction.ts +50 -0
- package/src/services/mandates/index.ts +4 -2
- package/src/services/mandates/policy.ts +20 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/browser.ts +48 -15
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +88 -6
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -35
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +33 -5
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/notifications/user.ts +6 -0
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +103 -10
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +115 -4
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/transport.ts +5 -1
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/core-settings.ts +18 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +48 -4
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/shared/redirect.ts +2 -1
- package/src/shared/template-rendering.ts +120 -7
- package/src/ssr/AppBadge.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +47 -26
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/IslandError.tsx +33 -0
- package/src/ssr/Layout.tsx +3 -0
- package/src/ssr/LayoutHelp.tsx +4 -2
- package/src/ssr/RailApps.island.tsx +9 -2
- package/src/ssr/app-badges.ts +93 -0
- package/src/ssr/platform-messages.ts +1 -1
- package/src/ssr/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +38 -17
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
- package/src/styles/utilities-navigation.css +41 -0
package/src/ai/chat/blocks.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { createSignal, For, type JSX, Match, Show, Switch, untrack } from "solid-js";
|
|
4
|
+
import { createSignal, createUniqueId, For, type JSX, Match, Show, Switch, untrack } from "solid-js";
|
|
5
5
|
import type { CapabilityActionReview } from "../../contracts/capabilities";
|
|
6
6
|
import { markdown } from "../../shared";
|
|
7
7
|
import type { AiTurnBlock } from "../protocol";
|
|
@@ -13,6 +13,7 @@ import { useAiChatActions } from "./message-actions";
|
|
|
13
13
|
import {
|
|
14
14
|
aiToolIcon,
|
|
15
15
|
capabilityErrorDescription,
|
|
16
|
+
codeCheckOutcome,
|
|
16
17
|
displayToolName,
|
|
17
18
|
fetchFileErrorPresentation,
|
|
18
19
|
formatToolDetailText,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
isTextEditorToolName,
|
|
23
24
|
jsonPreview,
|
|
24
25
|
memoryToolPresentation,
|
|
26
|
+
viewedChatImage,
|
|
25
27
|
} from "./message-utils";
|
|
26
28
|
import { aiChatMessages } from "./messages";
|
|
27
29
|
import { AssistantMarkdownBlock } from "./primitives";
|
|
@@ -200,7 +202,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
200
202
|
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
201
203
|
if (!actionDisabled() && !approval.loading()) void approval.mutate(input);
|
|
202
204
|
};
|
|
203
|
-
const title = () => props.block.presentation?.title ?? displayToolName(props.block.name);
|
|
205
|
+
const title = () => props.block.presentation?.title ?? displayToolName(props.block.name, locale());
|
|
204
206
|
const ownerName = () => props.block.presentation?.appName ?? t().assistant;
|
|
205
207
|
const description = () => {
|
|
206
208
|
const reviewMessage = props.block.approval?.review?.message.trim();
|
|
@@ -283,7 +285,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
283
285
|
<div class="mt-4 flex flex-col gap-4 text-xs leading-5 text-secondary">
|
|
284
286
|
<p class="whitespace-pre-wrap">{description()}</p>
|
|
285
287
|
<Show when={inlineReviewDetails().length > 0}>
|
|
286
|
-
<dl class="grid gap-x-5 gap-y-1.5
|
|
288
|
+
<dl class="grid gap-x-5 gap-y-1.5 sm:grid-cols-[max-content_minmax(0,1fr)]">
|
|
287
289
|
<For each={inlineReviewDetails()}>
|
|
288
290
|
{(detail) => (
|
|
289
291
|
<>
|
|
@@ -313,7 +315,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
313
315
|
</pre>
|
|
314
316
|
}
|
|
315
317
|
>
|
|
316
|
-
<div
|
|
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
|
+
>
|
|
317
324
|
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
318
325
|
</div>
|
|
319
326
|
</Show>
|
|
@@ -323,10 +330,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
323
330
|
</div>
|
|
324
331
|
</Show>
|
|
325
332
|
</div>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
333
|
+
{/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
|
|
334
|
+
<footer class="flex min-h-12 flex-wrap items-center gap-2 px-4 pb-3" data-ai-approval-footer>
|
|
335
|
+
{/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
|
|
336
|
+
<span class="k2b-sr-only" role="status" aria-live="polite">
|
|
337
|
+
{approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
|
|
338
|
+
</span>
|
|
330
339
|
<Show when={reviewLinks().length > 0}>
|
|
331
340
|
<nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
|
|
332
341
|
<For each={reviewLinks()}>
|
|
@@ -339,7 +348,9 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
339
348
|
</nav>
|
|
340
349
|
</Show>
|
|
341
350
|
<Show when={approval.error()}>
|
|
342
|
-
<p class="text-xs text-red-700 dark:text-red-300">
|
|
351
|
+
<p class="text-xs text-red-700 dark:text-red-300" role="alert">
|
|
352
|
+
{t().approvalFailed}
|
|
353
|
+
</p>
|
|
343
354
|
</Show>
|
|
344
355
|
<div class="ml-auto shrink-0">
|
|
345
356
|
<Show
|
|
@@ -526,7 +537,7 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
526
537
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
527
538
|
const locale = useLocale();
|
|
528
539
|
const presentation = () => props.block.presentation;
|
|
529
|
-
const title = () => presentation()?.title ?? displayToolName(props.block.name);
|
|
540
|
+
const title = () => presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
530
541
|
return (
|
|
531
542
|
<Chat.Activity
|
|
532
543
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
@@ -655,7 +666,7 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
655
666
|
fallback={
|
|
656
667
|
<ToolResultDisclosure
|
|
657
668
|
blockId={props.block.id}
|
|
658
|
-
name={displayToolName(props.block.name)}
|
|
669
|
+
name={displayToolName(props.block.name, locale())}
|
|
659
670
|
toolName={props.block.name}
|
|
660
671
|
args={props.block.args}
|
|
661
672
|
result={props.block.result}
|
|
@@ -709,11 +720,14 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
709
720
|
<TextEditorToolView turnId={props.turnId} block={props.block} active={props.active} />
|
|
710
721
|
</Match>
|
|
711
722
|
<Match when={status() === "running" || status() === "awaiting_client"}>
|
|
712
|
-
<Chat.Activity label={displayToolName(props.block.name)} icon={aiToolIcon(props.block.name)} busy />
|
|
723
|
+
<Chat.Activity label={displayToolName(props.block.name, locale())} icon={aiToolIcon(props.block.name)} busy />
|
|
713
724
|
</Match>
|
|
714
725
|
</Switch>
|
|
715
726
|
<Show when={hasCapabilityTable(props.block)}>
|
|
716
|
-
<CapabilityTablePreview
|
|
727
|
+
<CapabilityTablePreview
|
|
728
|
+
result={props.block.result}
|
|
729
|
+
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
730
|
+
/>
|
|
717
731
|
</Show>
|
|
718
732
|
</>
|
|
719
733
|
);
|
|
@@ -742,7 +756,56 @@ export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; str
|
|
|
742
756
|
);
|
|
743
757
|
}
|
|
744
758
|
|
|
745
|
-
/**
|
|
759
|
+
/** An alt text is a short description: the first line of the image description, clipped at a word. */
|
|
760
|
+
const altText = (description: string): string => {
|
|
761
|
+
const line =
|
|
762
|
+
description
|
|
763
|
+
.split("\n")
|
|
764
|
+
.find((entry) => entry.trim())
|
|
765
|
+
?.trim() ?? "";
|
|
766
|
+
if (line.length <= 240) return line;
|
|
767
|
+
const clipped = line.slice(0, 240);
|
|
768
|
+
return `${clipped.slice(0, Math.max(clipped.lastIndexOf(" "), 160))}…`;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* The image a view_image step looked at, as a small fixed-size thumbnail: it loads lazily through the chat's own file
|
|
773
|
+
* route, so it shows only what the viewer may read, and it keeps its box while it loads or fails. A host that opens
|
|
774
|
+
* chat files shows it larger on click; the button is named for that action and described by the image's alt text.
|
|
775
|
+
*/
|
|
776
|
+
function StepImage(props: { path: string; src: string; description: string }) {
|
|
777
|
+
const actions = useAiChatActions();
|
|
778
|
+
const locale = useLocale();
|
|
779
|
+
const imageId = createUniqueId();
|
|
780
|
+
const [failed, setFailed] = createSignal(false);
|
|
781
|
+
const name = () => props.path.slice(props.path.lastIndexOf("/") + 1) || props.path;
|
|
782
|
+
const alt = () => altText(props.description);
|
|
783
|
+
const image = () => (
|
|
784
|
+
<Show when={!failed()} fallback={<i class="ti ti-photo-off text-lg text-dimmed" aria-hidden="true" />}>
|
|
785
|
+
<img id={imageId} src={props.src} alt={alt() || name()} loading="lazy" decoding="async" onError={() => setFailed(true)} />
|
|
786
|
+
</Show>
|
|
787
|
+
);
|
|
788
|
+
return (
|
|
789
|
+
<Show when={actions.onOpenFile} fallback={<div class="ai-step-image">{image()}</div>}>
|
|
790
|
+
{(open) => (
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
class="ai-step-image focus-ui"
|
|
794
|
+
aria-label={aiChatMessages(locale()).openImage({ name: name() })}
|
|
795
|
+
aria-describedby={!failed() && alt() ? imageId : undefined}
|
|
796
|
+
onClick={() => open()(props.path)}
|
|
797
|
+
>
|
|
798
|
+
{image()}
|
|
799
|
+
</button>
|
|
800
|
+
)}
|
|
801
|
+
</Show>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* One step in the expanded work list: its title, target, state, and outcome, with input and output on demand. An app
|
|
807
|
+
* check names its outcome in words and with its own icon, so a passed and a failed check differ without colour.
|
|
808
|
+
*/
|
|
746
809
|
export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
747
810
|
const locale = useLocale();
|
|
748
811
|
const t = () => aiChatMessages(locale());
|
|
@@ -751,28 +814,54 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
751
814
|
const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
|
|
752
815
|
const state = () =>
|
|
753
816
|
waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
|
|
817
|
+
const finished = () => props.block.status === "completed" && !props.block.isError;
|
|
818
|
+
const check = () => (finished() ? codeCheckOutcome(props.block.name, props.block.result) : null);
|
|
819
|
+
const outcome = () => {
|
|
820
|
+
const value = check();
|
|
821
|
+
if (!value) return "";
|
|
822
|
+
const result = value.passed ? t().checkPassed : value.errors > 0 ? t().checkFindings({ count: value.errors }) : t().checkNotPassed;
|
|
823
|
+
return value.warnings > 0 ? `${result} · ${t().checkWarnings({ count: value.warnings })}` : result;
|
|
824
|
+
};
|
|
825
|
+
const actions = useAiChatActions();
|
|
826
|
+
/** Only a host that resolves chat file URLs shows the image; without one there is no preview, not an empty box. */
|
|
827
|
+
const image = () => {
|
|
828
|
+
const viewed = finished() ? viewedChatImage(props.block.name, props.block.result) : null;
|
|
829
|
+
const src = viewed ? actions.fileUrl?.(viewed.path) : null;
|
|
830
|
+
return viewed && src ? { ...viewed, src } : null;
|
|
831
|
+
};
|
|
832
|
+
const icon = () => {
|
|
833
|
+
if (waiting()) return "ti ti-clock";
|
|
834
|
+
const value = check();
|
|
835
|
+
if (value) return value.passed ? "ti ti-circle-check" : "ti ti-alert-triangle";
|
|
836
|
+
return aiToolIcon(props.block.name, props.block.presentation?.appIcon);
|
|
837
|
+
};
|
|
754
838
|
return (
|
|
755
839
|
<AiToolActivity
|
|
756
840
|
blockId={props.block.id}
|
|
757
|
-
label={props.block.presentation?.title ?? displayToolName(props.block.name)}
|
|
841
|
+
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
758
842
|
description={[
|
|
759
843
|
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
760
844
|
typeof detail() === "string" ? String(detail()) : "",
|
|
761
845
|
state(),
|
|
846
|
+
outcome(),
|
|
762
847
|
]
|
|
763
848
|
.filter(Boolean)
|
|
764
849
|
.join(" · ")}
|
|
765
|
-
icon={
|
|
850
|
+
icon={icon()}
|
|
766
851
|
busy={props.busy && props.block.status === "running"}
|
|
767
852
|
renderBody={() => (
|
|
768
|
-
<div
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
853
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
854
|
+
<div
|
|
855
|
+
class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
|
|
856
|
+
tabIndex={0}
|
|
857
|
+
role="region"
|
|
858
|
+
aria-label={t().toolInputOutput}
|
|
859
|
+
>
|
|
860
|
+
<ToolDetail title={t().input} toolName={props.block.name} value={props.block.args} />
|
|
861
|
+
<ToolDetail title={t().output} toolName={props.block.name} value={props.block.result} />
|
|
862
|
+
</div>
|
|
863
|
+
{/* Below the input and output, so an image that arrives in an open step grows in like its output and moves nothing above. */}
|
|
864
|
+
<Show when={image()}>{(viewed) => <StepImage path={viewed().path} src={viewed().src} description={viewed().description} />}</Show>
|
|
776
865
|
</div>
|
|
777
866
|
)}
|
|
778
867
|
/>
|
|
@@ -106,6 +106,7 @@ function SearchToolsView(props: { block: ToolBlock }) {
|
|
|
106
106
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
107
107
|
const tools = () => records(result().tools);
|
|
108
108
|
const t = useMessages();
|
|
109
|
+
const locale = useLocale();
|
|
109
110
|
const query = () => text(args().query) || t().tools;
|
|
110
111
|
return (
|
|
111
112
|
<CompletedActivity block={props.block} label={t().searchTools({ query: query() })} description={t().found({ count: tools().length })}>
|
|
@@ -115,7 +116,8 @@ function SearchToolsView(props: { block: ToolBlock }) {
|
|
|
115
116
|
{(tool) => (
|
|
116
117
|
<ResultRow
|
|
117
118
|
icon={tool.kind === "action" ? "ti-bolt" : tool.kind === "query" ? "ti-search" : aiToolIcon(text(tool.name))}
|
|
118
|
-
|
|
119
|
+
// The catalog titles a built-in tool in English, so the reader's language names it; a capability brings its own title.
|
|
120
|
+
title={tool.kind !== "builtin" && text(tool.title) ? text(tool.title) : displayToolName(text(tool.name), locale())}
|
|
119
121
|
description={text(tool.description)}
|
|
120
122
|
meta={text(tool.appId) || text(tool.kind)}
|
|
121
123
|
/>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type PendingAiVfsFile,
|
|
12
12
|
readImageFile,
|
|
13
13
|
readVfsFile,
|
|
14
|
+
VFS_FILE_MAX_BYTES,
|
|
14
15
|
} from "./message-utils";
|
|
15
16
|
|
|
16
17
|
export type { AiComposerAttachment } from "./message-utils";
|
|
@@ -37,6 +38,13 @@ export type AiComposerFileResult = {
|
|
|
37
38
|
|
|
38
39
|
export const aiComposerFileAccept = FILE_INPUT_ACCEPT;
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The `maxBytes` for `chooseFiles` while a composer holds `attached` attachments. Files from Cloud apps are read into
|
|
43
|
+
* the browser before `readAiComposerFiles` checks them, so together they stay within what the draft can still take:
|
|
44
|
+
* each free slot at most the largest file the composer attaches. 0 when every slot is taken.
|
|
45
|
+
*/
|
|
46
|
+
export const aiComposerChooseMaxBytes = (attached: number): number => Math.max(0, MAX_ATTACHMENTS - attached) * VFS_FILE_MAX_BYTES;
|
|
47
|
+
|
|
40
48
|
export const aiChatModelOptions = (profiles: readonly AiPublicModelProfile[]): ChatModelOption[] =>
|
|
41
49
|
profiles.map((profile) => ({
|
|
42
50
|
id: profile.id,
|
|
@@ -9,12 +9,18 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
9
9
|
/**
|
|
10
10
|
* The live chat timeline for a browser test, folded from wire events like the
|
|
11
11
|
* controller does. `window.emit(event)` applies one event,
|
|
12
|
-
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
|
-
* `window.reconnect(on)` sets whether the stream reconnects
|
|
12
|
+
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
|
+
* `window.reconnect(on)` sets whether the stream reconnects,
|
|
14
|
+
* `window.continued` lists the messages that **Continue** sent, and
|
|
15
|
+
* `window.opened` lists the chat files the timeline opened. Chat files load from `/files?path=`.
|
|
14
16
|
*/
|
|
15
17
|
const [state, setState] = createStore(emptyProjection());
|
|
16
18
|
const [reconnecting, setReconnecting] = createSignal(false);
|
|
19
|
+
const continued: string[] = [];
|
|
20
|
+
const opened: string[] = [];
|
|
17
21
|
Object.assign(window, {
|
|
22
|
+
continued,
|
|
23
|
+
opened,
|
|
18
24
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
19
25
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
20
26
|
steer: (text: string) =>
|
|
@@ -29,7 +35,13 @@ root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max
|
|
|
29
35
|
document.body.append(root);
|
|
30
36
|
render(
|
|
31
37
|
() => (
|
|
32
|
-
<AiChatActionsProvider
|
|
38
|
+
<AiChatActionsProvider
|
|
39
|
+
actions={{
|
|
40
|
+
onContinueTurn: (message) => void continued.push(message),
|
|
41
|
+
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
42
|
+
onOpenFile: (path) => void opened.push(path),
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
33
45
|
{(() => {
|
|
34
46
|
const items = createAiChatTimeline({
|
|
35
47
|
messages: createMemo(() => visibleMessages(state)),
|