@k2b/cloud 0.28.0 → 0.29.1
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 +9 -6
- 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/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +259 -197
- 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 +16 -3
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +28 -6
- package/src/ai/chat/messages.ts +20 -24
- package/src/ai/chat/presentation.tsx +1 -8
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-layout.ts +14 -2
- package/src/ai/chat/turn-view.tsx +132 -70
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/code-mode-skill.ts +4 -4
- package/src/ai/compaction.ts +22 -3
- package/src/ai/default-tools.ts +9 -2
- package/src/ai/executor.ts +127 -40
- 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 +1 -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/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +43 -5
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +20 -10
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +11 -3
- package/src/ai/store.ts +76 -33
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +5 -1
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +5 -1
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/types.ts +3 -1
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +71 -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 +64 -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/outgoing-mail.ts +30 -2
- 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 +56 -11
- 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/image-metadata.ts +438 -0
- package/src/services/index.ts +1 -0
- 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/browser.ts +48 -15
- package/src/services/notifications/dispatcher.ts +31 -1
- package/src/services/notifications/platform.ts +32 -4
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/user.ts +6 -0
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +2 -2
- package/src/shared/network-address.ts +1 -0
- 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/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/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +165 -0
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/utilities-markdown-table.css +5 -1
- 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";
|
|
@@ -181,7 +183,14 @@ function ToolResultDisclosure(props: {
|
|
|
181
183
|
);
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
/** A decision the server accepted, for the call it answered. */
|
|
187
|
+
export type AiApprovalDecision = { callId: string; approved: boolean };
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The pending approval: a calm tinted card that says what will happen, with its fields and the decision. `onDecided`
|
|
191
|
+
* hears an accepted decision with the call it answered, so the turn can show its receipt in the card's place right away.
|
|
192
|
+
*/
|
|
193
|
+
export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onDecided?: (decision: AiApprovalDecision) => void }) {
|
|
185
194
|
const actions = useAiChatActions();
|
|
186
195
|
const locale = useLocale();
|
|
187
196
|
const t = () => aiChatMessages(locale());
|
|
@@ -190,17 +199,24 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
190
199
|
const actionDisabled = () => actions.actionDisabled?.() ?? false;
|
|
191
200
|
const [submitted, setSubmitted] = createSignal(false);
|
|
192
201
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
|
202
|
+
// The call this card answered: a later approval for the same block may already have arrived when the server accepts.
|
|
203
|
+
let decision: AiApprovalDecision | null = null;
|
|
193
204
|
const approval = mutation.create<void, { approved: boolean; remember?: "always" }>({
|
|
194
205
|
mutation: async (input) => {
|
|
195
206
|
if (!actions.onApproval) throw new Error("Approval is unavailable.");
|
|
196
207
|
await actions.onApproval(request(), input);
|
|
197
208
|
},
|
|
198
|
-
onSuccess: () =>
|
|
209
|
+
onSuccess: () => {
|
|
210
|
+
setSubmitted(true);
|
|
211
|
+
if (decision) props.onDecided?.(decision);
|
|
212
|
+
},
|
|
199
213
|
});
|
|
200
214
|
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
201
|
-
if (
|
|
215
|
+
if (actionDisabled() || approval.loading()) return;
|
|
216
|
+
decision = { callId: props.block.callId, approved: input.approved };
|
|
217
|
+
void approval.mutate(input);
|
|
202
218
|
};
|
|
203
|
-
const title = () => props.block.presentation?.title ?? displayToolName(props.block.name);
|
|
219
|
+
const title = () => props.block.presentation?.title ?? displayToolName(props.block.name, locale());
|
|
204
220
|
const ownerName = () => props.block.presentation?.appName ?? t().assistant;
|
|
205
221
|
const description = () => {
|
|
206
222
|
const reviewMessage = props.block.approval?.review?.message.trim();
|
|
@@ -237,199 +253,166 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
237
253
|
download: t().download,
|
|
238
254
|
})[rel];
|
|
239
255
|
const detailData = () => (isStructuredDataValue(props.block.args) ? props.block.args : undefined);
|
|
240
|
-
const appAccent = () => props.block.presentation?.appAccent;
|
|
241
256
|
const detailsId = `approval-details-${props.block.callId}`;
|
|
242
257
|
return (
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
class={`
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<
|
|
284
|
-
<
|
|
285
|
-
<Show
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
258
|
+
<section class="ai-approval" aria-label={t().approvalRequired({ title: title() })}>
|
|
259
|
+
<div class="ai-approval__head">
|
|
260
|
+
<i class={`ai-approval__icon ${aiToolIcon(props.block.name, props.block.presentation?.appIcon)}`} aria-hidden="true" />
|
|
261
|
+
<div class="min-w-0">
|
|
262
|
+
<h3 class="ai-approval__title">
|
|
263
|
+
{ownerName()} · {title()}
|
|
264
|
+
</h3>
|
|
265
|
+
<p class="ai-approval__sub">{t().approvalRunsAfter}</p>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<Show when={reviewLines().length > 0}>
|
|
269
|
+
<div class="ai-approval__text">
|
|
270
|
+
<For each={reviewLines()}>
|
|
271
|
+
{(line) => (
|
|
272
|
+
<p class="whitespace-pre-wrap">
|
|
273
|
+
<Show when={line.label}>{(label) => <strong class="ai-approval__label">{label()}: </strong>}</Show>
|
|
274
|
+
{line.value}
|
|
275
|
+
</p>
|
|
276
|
+
)}
|
|
277
|
+
</For>
|
|
278
|
+
</div>
|
|
279
|
+
</Show>
|
|
280
|
+
<Show when={props.block.approval?.review}>
|
|
281
|
+
<p class="ai-approval__text whitespace-pre-wrap">{description()}</p>
|
|
282
|
+
<Show when={inlineReviewDetails().length > 0}>
|
|
283
|
+
<dl class="ai-approval__fields">
|
|
284
|
+
<For each={inlineReviewDetails()}>
|
|
285
|
+
{(detail) => (
|
|
286
|
+
<>
|
|
287
|
+
<dt>{detail.label}</dt>
|
|
288
|
+
<dd class="whitespace-pre-wrap">
|
|
289
|
+
<ReviewDetailValue detail={detail} />
|
|
290
|
+
</dd>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</For>
|
|
294
|
+
</dl>
|
|
295
|
+
</Show>
|
|
296
|
+
<For each={blockReviewDetails()}>
|
|
297
|
+
{(detail) => (
|
|
298
|
+
<section class="ai-approval__text min-w-0" aria-label={detail.label}>
|
|
299
|
+
<h4 class="ai-approval__label mb-1">{detail.label}</h4>
|
|
300
|
+
<Show
|
|
301
|
+
when={detail.format === "markdown"}
|
|
302
|
+
fallback={
|
|
303
|
+
<pre
|
|
304
|
+
class="ai-approval__preview whitespace-pre-wrap break-words font-sans"
|
|
305
|
+
role="region"
|
|
306
|
+
tabIndex={0}
|
|
307
|
+
aria-label={t().contentOf({ label: detail.label })}
|
|
308
|
+
>
|
|
309
|
+
<ReviewDetailValue detail={detail} />
|
|
310
|
+
</pre>
|
|
311
|
+
}
|
|
312
|
+
>
|
|
313
|
+
<div class="ai-approval__preview" role="region" tabIndex={0} aria-label={detail.label}>
|
|
314
|
+
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
315
|
+
</div>
|
|
298
316
|
</Show>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
class="max-h-72 overflow-auto whitespace-pre-wrap break-words pr-2 font-sans text-xs leading-5 text-secondary"
|
|
308
|
-
role="region"
|
|
309
|
-
tabIndex={0}
|
|
310
|
-
aria-label={t().contentOf({ label: detail.label })}
|
|
311
|
-
>
|
|
312
|
-
<ReviewDetailValue detail={detail} />
|
|
313
|
-
</pre>
|
|
314
|
-
}
|
|
315
|
-
>
|
|
316
|
-
<div
|
|
317
|
-
class="-mx-2 max-h-72 overflow-auto px-2 text-xs text-secondary"
|
|
318
|
-
role="region"
|
|
319
|
-
tabIndex={0}
|
|
320
|
-
aria-label={detail.label}
|
|
321
|
-
>
|
|
322
|
-
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
323
|
-
</div>
|
|
324
|
-
</Show>
|
|
325
|
-
</section>
|
|
326
|
-
)}
|
|
327
|
-
</For>
|
|
328
|
-
</div>
|
|
317
|
+
</section>
|
|
318
|
+
)}
|
|
319
|
+
</For>
|
|
320
|
+
</Show>
|
|
321
|
+
<Show when={detailsOpen()}>
|
|
322
|
+
<div id={detailsId} class="w-full min-w-0" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
323
|
+
<Show when={detailData()} fallback={<pre class="ai-approval__preview text-[11px]">{jsonPreview(props.block.args)}</pre>}>
|
|
324
|
+
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
329
325
|
</Show>
|
|
330
326
|
</div>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</
|
|
347
|
-
</
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
</
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
327
|
+
</Show>
|
|
328
|
+
{/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
|
|
329
|
+
<footer class="ai-approval__foot" data-ai-approval-footer>
|
|
330
|
+
{/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
|
|
331
|
+
<span class="k2b-sr-only" role="status" aria-live="polite">
|
|
332
|
+
{approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
|
|
333
|
+
</span>
|
|
334
|
+
<Show when={reviewLinks().length > 0}>
|
|
335
|
+
<nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
|
|
336
|
+
<For each={reviewLinks()}>
|
|
337
|
+
{(link) => (
|
|
338
|
+
<ButtonLink href={link.href} target="_blank" rel="noopener noreferrer" size="xs" variant="ghost">
|
|
339
|
+
{link.title ?? reviewLinkTitle(link.rel)}
|
|
340
|
+
</ButtonLink>
|
|
341
|
+
)}
|
|
342
|
+
</For>
|
|
343
|
+
</nav>
|
|
344
|
+
</Show>
|
|
345
|
+
<Show when={approval.error()}>
|
|
346
|
+
<p class="text-xs text-red-700 dark:text-red-300" role="alert">
|
|
347
|
+
{t().approvalFailed}
|
|
348
|
+
</p>
|
|
349
|
+
</Show>
|
|
350
|
+
<div class="ai-approval__actions">
|
|
351
|
+
<Show
|
|
352
|
+
when={pending()}
|
|
353
|
+
fallback={
|
|
354
|
+
<span class="ai-approval__state">
|
|
355
|
+
{title()} · {props.block.status === "rejected" ? t().rejected : t().approved}
|
|
356
|
+
</span>
|
|
357
|
+
}
|
|
358
|
+
>
|
|
359
|
+
<Show when={actions.onApproval} fallback={<span class="ai-approval__state">{t().approvalUnavailable}</span>}>
|
|
360
|
+
<Show
|
|
361
|
+
when={!actionDisabled()}
|
|
362
|
+
fallback={
|
|
363
|
+
<span class="ai-approval__state">
|
|
364
|
+
<i class="ti ti-player-stop" aria-hidden="true" />
|
|
365
|
+
{t().stoppingResponse}
|
|
366
|
+
</span>
|
|
367
|
+
}
|
|
368
|
+
>
|
|
363
369
|
<Show
|
|
364
|
-
when={!
|
|
370
|
+
when={!submitted()}
|
|
365
371
|
fallback={
|
|
366
|
-
<span class="
|
|
367
|
-
<i class="ti ti-
|
|
368
|
-
{t().
|
|
372
|
+
<span class="ai-approval__state">
|
|
373
|
+
<i class="ti ti-check" aria-hidden="true" />
|
|
374
|
+
{t().submitted}
|
|
369
375
|
</span>
|
|
370
376
|
}
|
|
371
377
|
>
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
{/* The buttons stay while the decision is sent, so the card does not change its height before it
|
|
379
|
+
becomes its receipt. */}
|
|
380
|
+
<Button size="sm" variant="ghost" disabled={approval.loading()} onClick={() => submit({ approved: false })}>
|
|
381
|
+
{t().reject}
|
|
382
|
+
</Button>
|
|
383
|
+
<SplitButton
|
|
384
|
+
size="sm"
|
|
385
|
+
variant="ai"
|
|
386
|
+
loading={approval.loading()}
|
|
387
|
+
onClick={() => submit({ approved: true })}
|
|
388
|
+
menuLabel={t().moreOptions({ title: title() })}
|
|
389
|
+
menuPosition="bottom-right"
|
|
390
|
+
items={[
|
|
391
|
+
{
|
|
392
|
+
label: detailsOpen() ? t().hideDetails : t().details,
|
|
393
|
+
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
394
|
+
action: () => setDetailsOpen((open) => !open),
|
|
395
|
+
},
|
|
396
|
+
...(props.block.approval?.allowAlways
|
|
397
|
+
? [
|
|
398
|
+
{
|
|
399
|
+
label: t().alwaysApprove,
|
|
400
|
+
icon: "ti ti-shield-check",
|
|
401
|
+
action: () => submit({ approved: true, remember: "always" }),
|
|
402
|
+
},
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
405
|
+
]}
|
|
380
406
|
>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
{t().reject}
|
|
384
|
-
</Button>
|
|
385
|
-
<SplitButton
|
|
386
|
-
size="xs"
|
|
387
|
-
variant="ai"
|
|
388
|
-
onClick={() => submit({ approved: true })}
|
|
389
|
-
menuLabel={t().moreOptions({ title: title() })}
|
|
390
|
-
menuPosition="bottom-right"
|
|
391
|
-
items={[
|
|
392
|
-
{
|
|
393
|
-
label: detailsOpen() ? t().hideDetails : t().details,
|
|
394
|
-
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
395
|
-
action: () => setDetailsOpen((open) => !open),
|
|
396
|
-
},
|
|
397
|
-
...(props.block.approval?.allowAlways
|
|
398
|
-
? [
|
|
399
|
-
{
|
|
400
|
-
label: t().alwaysApprove,
|
|
401
|
-
icon: "ti ti-shield-check",
|
|
402
|
-
action: () => submit({ approved: true, remember: "always" }),
|
|
403
|
-
},
|
|
404
|
-
]
|
|
405
|
-
: []),
|
|
406
|
-
]}
|
|
407
|
-
>
|
|
408
|
-
{title()}
|
|
409
|
-
</SplitButton>
|
|
410
|
-
</div>
|
|
411
|
-
</Show>
|
|
407
|
+
{title()}
|
|
408
|
+
</SplitButton>
|
|
412
409
|
</Show>
|
|
413
410
|
</Show>
|
|
414
411
|
</Show>
|
|
415
|
-
</div>
|
|
416
|
-
</footer>
|
|
417
|
-
</section>
|
|
418
|
-
<Show when={detailsOpen()}>
|
|
419
|
-
<div id={detailsId} class="mt-2 w-full" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
420
|
-
<Show
|
|
421
|
-
when={detailData()}
|
|
422
|
-
fallback={
|
|
423
|
-
<pre class="max-h-40 overflow-auto rounded-md bg-white/55 p-2 text-[11px] text-primary dark:bg-black/20">
|
|
424
|
-
{jsonPreview(props.block.args)}
|
|
425
|
-
</pre>
|
|
426
|
-
}
|
|
427
|
-
>
|
|
428
|
-
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
429
412
|
</Show>
|
|
430
413
|
</div>
|
|
431
|
-
</
|
|
432
|
-
</
|
|
414
|
+
</footer>
|
|
415
|
+
</section>
|
|
433
416
|
);
|
|
434
417
|
}
|
|
435
418
|
|
|
@@ -535,7 +518,7 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
535
518
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
536
519
|
const locale = useLocale();
|
|
537
520
|
const presentation = () => props.block.presentation;
|
|
538
|
-
const title = () => presentation()?.title ?? displayToolName(props.block.name);
|
|
521
|
+
const title = () => presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
539
522
|
return (
|
|
540
523
|
<Chat.Activity
|
|
541
524
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
@@ -664,7 +647,7 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
664
647
|
fallback={
|
|
665
648
|
<ToolResultDisclosure
|
|
666
649
|
blockId={props.block.id}
|
|
667
|
-
name={displayToolName(props.block.name)}
|
|
650
|
+
name={displayToolName(props.block.name, locale())}
|
|
668
651
|
toolName={props.block.name}
|
|
669
652
|
args={props.block.args}
|
|
670
653
|
result={props.block.result}
|
|
@@ -718,11 +701,14 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
718
701
|
<TextEditorToolView turnId={props.turnId} block={props.block} active={props.active} />
|
|
719
702
|
</Match>
|
|
720
703
|
<Match when={status() === "running" || status() === "awaiting_client"}>
|
|
721
|
-
<Chat.Activity label={displayToolName(props.block.name)} icon={aiToolIcon(props.block.name)} busy />
|
|
704
|
+
<Chat.Activity label={displayToolName(props.block.name, locale())} icon={aiToolIcon(props.block.name)} busy />
|
|
722
705
|
</Match>
|
|
723
706
|
</Switch>
|
|
724
707
|
<Show when={hasCapabilityTable(props.block)}>
|
|
725
|
-
<CapabilityTablePreview
|
|
708
|
+
<CapabilityTablePreview
|
|
709
|
+
result={props.block.result}
|
|
710
|
+
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
711
|
+
/>
|
|
726
712
|
</Show>
|
|
727
713
|
</>
|
|
728
714
|
);
|
|
@@ -751,7 +737,56 @@ export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; str
|
|
|
751
737
|
);
|
|
752
738
|
}
|
|
753
739
|
|
|
754
|
-
/**
|
|
740
|
+
/** An alt text is a short description: the first line of the image description, clipped at a word. */
|
|
741
|
+
const altText = (description: string): string => {
|
|
742
|
+
const line =
|
|
743
|
+
description
|
|
744
|
+
.split("\n")
|
|
745
|
+
.find((entry) => entry.trim())
|
|
746
|
+
?.trim() ?? "";
|
|
747
|
+
if (line.length <= 240) return line;
|
|
748
|
+
const clipped = line.slice(0, 240);
|
|
749
|
+
return `${clipped.slice(0, Math.max(clipped.lastIndexOf(" "), 160))}…`;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* The image a view_image step looked at, as a small fixed-size thumbnail: it loads lazily through the chat's own file
|
|
754
|
+
* route, so it shows only what the viewer may read, and it keeps its box while it loads or fails. A host that opens
|
|
755
|
+
* chat files shows it larger on click; the button is named for that action and described by the image's alt text.
|
|
756
|
+
*/
|
|
757
|
+
function StepImage(props: { path: string; src: string; description: string }) {
|
|
758
|
+
const actions = useAiChatActions();
|
|
759
|
+
const locale = useLocale();
|
|
760
|
+
const imageId = createUniqueId();
|
|
761
|
+
const [failed, setFailed] = createSignal(false);
|
|
762
|
+
const name = () => props.path.slice(props.path.lastIndexOf("/") + 1) || props.path;
|
|
763
|
+
const alt = () => altText(props.description);
|
|
764
|
+
const image = () => (
|
|
765
|
+
<Show when={!failed()} fallback={<i class="ti ti-photo-off text-lg text-dimmed" aria-hidden="true" />}>
|
|
766
|
+
<img id={imageId} src={props.src} alt={alt() || name()} loading="lazy" decoding="async" onError={() => setFailed(true)} />
|
|
767
|
+
</Show>
|
|
768
|
+
);
|
|
769
|
+
return (
|
|
770
|
+
<Show when={actions.onOpenFile} fallback={<div class="ai-step-image">{image()}</div>}>
|
|
771
|
+
{(open) => (
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
class="ai-step-image focus-ui"
|
|
775
|
+
aria-label={aiChatMessages(locale()).openImage({ name: name() })}
|
|
776
|
+
aria-describedby={!failed() && alt() ? imageId : undefined}
|
|
777
|
+
onClick={() => open()(props.path)}
|
|
778
|
+
>
|
|
779
|
+
{image()}
|
|
780
|
+
</button>
|
|
781
|
+
)}
|
|
782
|
+
</Show>
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* One step in the expanded work list: its title, target, state, and outcome, with input and output on demand. An app
|
|
788
|
+
* check names its outcome in words and with its own icon, so a passed and a failed check differ without colour.
|
|
789
|
+
*/
|
|
755
790
|
export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
756
791
|
const locale = useLocale();
|
|
757
792
|
const t = () => aiChatMessages(locale());
|
|
@@ -760,28 +795,55 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
760
795
|
const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
|
|
761
796
|
const state = () =>
|
|
762
797
|
waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
|
|
798
|
+
const finished = () => props.block.status === "completed" && !props.block.isError;
|
|
799
|
+
const check = () => (finished() ? codeCheckOutcome(props.block.name, props.block.result) : null);
|
|
800
|
+
const outcome = () => {
|
|
801
|
+
const value = check();
|
|
802
|
+
if (!value) return "";
|
|
803
|
+
const result = value.passed ? t().checkPassed : value.errors > 0 ? t().checkFindings({ count: value.errors }) : t().checkNotPassed;
|
|
804
|
+
return value.warnings > 0 ? `${result} · ${t().checkWarnings({ count: value.warnings })}` : result;
|
|
805
|
+
};
|
|
806
|
+
const actions = useAiChatActions();
|
|
807
|
+
/** Only a host that resolves chat file URLs shows the image; without one there is no preview, not an empty box. */
|
|
808
|
+
const image = () => {
|
|
809
|
+
const viewed = finished() ? viewedChatImage(props.block.name, props.block.result) : null;
|
|
810
|
+
const src = viewed ? actions.fileUrl?.(viewed.path) : null;
|
|
811
|
+
return viewed && src ? { ...viewed, src } : null;
|
|
812
|
+
};
|
|
813
|
+
const icon = () => {
|
|
814
|
+
if (waiting()) return "ti ti-clock";
|
|
815
|
+
const value = check();
|
|
816
|
+
if (value) return value.passed ? "ti ti-circle-check" : "ti ti-alert-triangle";
|
|
817
|
+
return aiToolIcon(props.block.name, props.block.presentation?.appIcon);
|
|
818
|
+
};
|
|
763
819
|
return (
|
|
764
820
|
<AiToolActivity
|
|
765
821
|
blockId={props.block.id}
|
|
766
|
-
label={props.block.presentation?.title ?? displayToolName(props.block.name)}
|
|
822
|
+
label={props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
767
823
|
description={[
|
|
768
824
|
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
769
825
|
typeof detail() === "string" ? String(detail()) : "",
|
|
770
826
|
state(),
|
|
827
|
+
outcome(),
|
|
771
828
|
]
|
|
772
829
|
.filter(Boolean)
|
|
773
830
|
.join(" · ")}
|
|
774
|
-
icon={
|
|
831
|
+
icon={icon()}
|
|
775
832
|
busy={props.busy && props.block.status === "running"}
|
|
833
|
+
bodyInset={false}
|
|
776
834
|
renderBody={() => (
|
|
777
|
-
<div
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
835
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
836
|
+
<div
|
|
837
|
+
class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
|
|
838
|
+
tabIndex={0}
|
|
839
|
+
role="region"
|
|
840
|
+
aria-label={t().toolInputOutput}
|
|
841
|
+
>
|
|
842
|
+
<ToolDetail title={t().input} toolName={props.block.name} value={props.block.args} />
|
|
843
|
+
<ToolDetail title={t().output} toolName={props.block.name} value={props.block.result} />
|
|
844
|
+
</div>
|
|
845
|
+
{/* Below the input and output, so an image that arrives in an open step grows in like its output and moves nothing above. */}
|
|
846
|
+
<Show when={image()}>{(viewed) => <StepImage path={viewed().path} src={viewed().src} description={viewed().description} />}</Show>
|
|
785
847
|
</div>
|
|
786
848
|
)}
|
|
787
849
|
/>
|
|
@@ -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,
|