@k2b/cloud 0.28.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/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +97 -17
- 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 +12 -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 +16 -0
- package/src/ai/chat/presentation.tsx +1 -8
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-layout.ts +12 -0
- package/src/ai/chat/turn-view.tsx +15 -7
- 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 +34 -1
- 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 +56 -20
- 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 +1 -0
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- 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/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/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/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/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 +23 -0
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/utilities-navigation.css +41 -0
|
@@ -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,
|
|
@@ -10,14 +10,17 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
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
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
|
|
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=`.
|
|
15
16
|
*/
|
|
16
17
|
const [state, setState] = createStore(emptyProjection());
|
|
17
18
|
const [reconnecting, setReconnecting] = createSignal(false);
|
|
18
19
|
const continued: string[] = [];
|
|
20
|
+
const opened: string[] = [];
|
|
19
21
|
Object.assign(window, {
|
|
20
22
|
continued,
|
|
23
|
+
opened,
|
|
21
24
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
22
25
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
23
26
|
steer: (text: string) =>
|
|
@@ -32,7 +35,13 @@ root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max
|
|
|
32
35
|
document.body.append(root);
|
|
33
36
|
render(
|
|
34
37
|
() => (
|
|
35
|
-
<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
|
+
>
|
|
36
45
|
{(() => {
|
|
37
46
|
const items = createAiChatTimeline({
|
|
38
47
|
messages: createMemo(() => visibleMessages(state)),
|
|
@@ -218,7 +218,7 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
218
218
|
{(tool) => (
|
|
219
219
|
<li>
|
|
220
220
|
<i class={aiToolIcon(tool.name)} aria-hidden="true" />
|
|
221
|
-
{displayToolName(tool.name)}
|
|
221
|
+
{displayToolName(tool.name, locale)}
|
|
222
222
|
<Show when={tool.count > 1}>
|
|
223
223
|
<span class="text-dimmed tabular-nums">×{tool.count}</span>
|
|
224
224
|
</Show>
|
|
@@ -3,12 +3,14 @@ import { fileIcons } from "@k2b/stdlib";
|
|
|
3
3
|
import { formatBytes as sharedFormatBytes } from "../../shared/format";
|
|
4
4
|
import { type AiAttachmentRef, parseAiAttachmentMarkers } from "../attachments";
|
|
5
5
|
import { AI_AUDIO_EXTENSIONS } from "../audio-format";
|
|
6
|
+
import { aiProjectFilePathFromMount, aiSkillFilePathFromMount } from "../file-mount";
|
|
6
7
|
import { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS } from "../limits";
|
|
7
8
|
import { type AiResourceMarker, parseAiResourceMarker } from "../resource-markers";
|
|
8
9
|
import { assistantVisibleTextFromMessage } from "../timeline";
|
|
9
10
|
import type { AiStoredMessage, AiUserContentPart } from "../types";
|
|
10
11
|
import { AI_IMAGE_MEDIA_TYPES, isAiImageMediaType } from "../types";
|
|
11
12
|
import { aiChatMessages } from "./messages";
|
|
13
|
+
import { aiBuiltinToolName } from "./tool-names";
|
|
12
14
|
|
|
13
15
|
type AssistantToolResultMessage = Extract<Message, { role: "tool_result" }>;
|
|
14
16
|
|
|
@@ -144,8 +146,9 @@ export const describeResponseEnd = (entry: Pick<AiStoredMessage, "loopDoneReason
|
|
|
144
146
|
interrupted: t.infoEndInterrupted,
|
|
145
147
|
error: t.infoEndFailed,
|
|
146
148
|
};
|
|
149
|
+
const stopped = entry.stopReason && ["error", "aborted", "interrupted"].includes(entry.stopReason);
|
|
147
150
|
const parts = [
|
|
148
|
-
entry.loopDoneReason ? run[entry.loopDoneReason] : null,
|
|
151
|
+
entry.loopDoneReason && !(entry.loopDoneReason === "stop" && stopped) ? run[entry.loopDoneReason] : null,
|
|
149
152
|
entry.stopReason ? (Object.hasOwn(message, entry.stopReason) ? message[entry.stopReason] : entry.stopReason) : null,
|
|
150
153
|
].filter((part): part is string => Boolean(part));
|
|
151
154
|
return [...new Set(parts)].join(" · ") || t.infoUnknown;
|
|
@@ -312,11 +315,10 @@ export const isSurveyToolName = (name: string) => name === "survey" || name ===
|
|
|
312
315
|
|
|
313
316
|
export const isTextEditorToolName = (name: string) => name === "text_editor" || name === "cloud_text_editor";
|
|
314
317
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
if (name === "local_bash") return "Local Bash";
|
|
318
|
+
/** A tool's name for people: the localized name of a built-in tool, otherwise its name in words. */
|
|
319
|
+
export const displayToolName = (name: string, locale: string) => {
|
|
320
|
+
const builtin = aiBuiltinToolName(name, locale);
|
|
321
|
+
if (builtin) return builtin;
|
|
320
322
|
const words = name.split("_").filter(Boolean).join(" ");
|
|
321
323
|
return `${words.slice(0, 1).toUpperCase()}${words.slice(1)}`;
|
|
322
324
|
};
|
|
@@ -340,6 +342,7 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
|
|
|
340
342
|
["web_search", "ti ti-search"],
|
|
341
343
|
["web_extract", "ti ti-world-download"],
|
|
342
344
|
["view_image", "ti ti-photo-spark"],
|
|
345
|
+
["code_check", "ti ti-checklist"],
|
|
343
346
|
["memory", "ti ti-brain"],
|
|
344
347
|
["search_project", "ti ti-folder-search"],
|
|
345
348
|
["read_project_knowledge", "ti ti-notebook"],
|
|
@@ -353,6 +356,25 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
|
|
|
353
356
|
["read_cloud_resource", "ti ti-ai-gateway"],
|
|
354
357
|
]);
|
|
355
358
|
|
|
359
|
+
/** What an app check found: errors stop it from passing, warnings are counted separately. */
|
|
360
|
+
export type AiCodeCheckOutcome = { passed: boolean; errors: number; warnings: number };
|
|
361
|
+
|
|
362
|
+
export const codeCheckOutcome = (name: string, result: unknown): AiCodeCheckOutcome | null => {
|
|
363
|
+
if (name !== "code_check" || !isRecord(result) || typeof result.passed !== "boolean") return null;
|
|
364
|
+
const issues = Array.isArray(result.issues) ? result.issues.filter(isRecord) : [];
|
|
365
|
+
const warnings = issues.filter((issue) => issue.severity === "warning").length;
|
|
366
|
+
return { passed: result.passed, errors: issues.length - warnings, warnings };
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** The chat image a view_image call looked at. Project and skill files are not chat files and have no preview here. */
|
|
370
|
+
export const viewedChatImage = (name: string, result: unknown): { path: string; description: string } | null => {
|
|
371
|
+
if (name !== "view_image" || !isRecord(result)) return null;
|
|
372
|
+
const { path, mediaType, description } = result;
|
|
373
|
+
if (typeof path !== "string" || typeof mediaType !== "string" || !isAiImageMediaType(mediaType)) return null;
|
|
374
|
+
if (aiProjectFilePathFromMount(path) !== null || aiSkillFilePathFromMount(path) !== null) return null;
|
|
375
|
+
return { path, description: typeof description === "string" ? description.trim() : "" };
|
|
376
|
+
};
|
|
377
|
+
|
|
356
378
|
export const aiToolIcon = (name: string, appIcon?: string | null): string => {
|
|
357
379
|
if (appIcon) return appIcon;
|
|
358
380
|
return BUILT_IN_TOOL_ICONS.get(name) ?? (/__(?:query|action)__/.test(name) ? "ti ti-ai-gateway" : "ti ti-tool");
|
package/src/ai/chat/messages.ts
CHANGED
|
@@ -25,6 +25,7 @@ const messages = i18n.define({
|
|
|
25
25
|
turnErrorTitle: "The answer was interrupted.",
|
|
26
26
|
turnErrorReason: ({ code, limitMinutes }: AiTurnError) => {
|
|
27
27
|
if (code === "model_unavailable") return "The model service did not answer.";
|
|
28
|
+
if (code === "provider_stopped") return "The model provider ended this answer early.";
|
|
28
29
|
if (code === "quota_exhausted") return "Your AI usage limit for this period is reached. You can continue once it resets.";
|
|
29
30
|
if (code === "context_full") return "This chat is too long for the model. Start a new chat to continue; this one stays as it is.";
|
|
30
31
|
if (code === "time_limit")
|
|
@@ -71,6 +72,7 @@ const messages = i18n.define({
|
|
|
71
72
|
stepWritingCode: "Writing code",
|
|
72
73
|
stepRunningCode: "Running code",
|
|
73
74
|
stepCheckingApp: "Checking the app",
|
|
75
|
+
stepCheckingRun: "Checking the run",
|
|
74
76
|
stepCode: "Working with code",
|
|
75
77
|
stepPlan: "Updating the working plan",
|
|
76
78
|
stepPdf: "Creating a PDF",
|
|
@@ -87,6 +89,11 @@ const messages = i18n.define({
|
|
|
87
89
|
stepFailed: "failed",
|
|
88
90
|
stepRejected: "rejected",
|
|
89
91
|
stepWaiting: "waiting",
|
|
92
|
+
checkPassed: "passed",
|
|
93
|
+
checkNotPassed: "not passed",
|
|
94
|
+
checkFindings: ({ count }: { count: number }) => (count === 1 ? "1 finding" : `${count} findings`),
|
|
95
|
+
checkWarnings: ({ count }: { count: number }) => (count === 1 ? "1 warning" : `${count} warnings`),
|
|
96
|
+
openImage: ({ name }: { name: string }) => `Open image ${name}`,
|
|
90
97
|
steered: "Conversation steered",
|
|
91
98
|
reasoning: "Reasoning",
|
|
92
99
|
groupPlan: "Updated working plan",
|
|
@@ -286,6 +293,7 @@ const messages = i18n.define({
|
|
|
286
293
|
`${count === 1 ? "1 message" : `${count} messages`} summarized · ${date}`,
|
|
287
294
|
compactedHint: "From here on, the model sees this summary instead of the messages above.",
|
|
288
295
|
compactedEmpty: "No visible content",
|
|
296
|
+
malformedImage: "This image could not be read. Export it again or choose another file.",
|
|
289
297
|
},
|
|
290
298
|
de: {
|
|
291
299
|
backgroundRun: "Hintergrundlauf",
|
|
@@ -309,6 +317,7 @@ const messages = i18n.define({
|
|
|
309
317
|
turnErrorTitle: "Die Antwort wurde abgebrochen.",
|
|
310
318
|
turnErrorReason: ({ code, limitMinutes }) => {
|
|
311
319
|
if (code === "model_unavailable") return "Der KI-Dienst hat nicht geantwortet.";
|
|
320
|
+
if (code === "provider_stopped") return "Der KI-Anbieter hat diese Antwort vorzeitig beendet.";
|
|
312
321
|
if (code === "quota_exhausted")
|
|
313
322
|
return "Dein KI-Kontingent für diesen Zeitraum ist aufgebraucht. Du kannst weiterarbeiten, sobald es sich erneuert.";
|
|
314
323
|
if (code === "context_full")
|
|
@@ -361,6 +370,7 @@ const messages = i18n.define({
|
|
|
361
370
|
stepWritingCode: "Schreibt Code",
|
|
362
371
|
stepRunningCode: "Führt Code aus",
|
|
363
372
|
stepCheckingApp: "Prüft die App",
|
|
373
|
+
stepCheckingRun: "Prüft den Lauf",
|
|
364
374
|
stepCode: "Arbeitet mit Code",
|
|
365
375
|
stepPlan: "Aktualisiert den Arbeitsplan",
|
|
366
376
|
stepPdf: "Erstellt ein PDF",
|
|
@@ -377,6 +387,11 @@ const messages = i18n.define({
|
|
|
377
387
|
stepFailed: "fehlgeschlagen",
|
|
378
388
|
stepRejected: "abgelehnt",
|
|
379
389
|
stepWaiting: "wartet",
|
|
390
|
+
checkPassed: "bestanden",
|
|
391
|
+
checkNotPassed: "nicht bestanden",
|
|
392
|
+
checkFindings: ({ count }) => (count === 1 ? "1 Befund" : `${count} Befunde`),
|
|
393
|
+
checkWarnings: ({ count }) => (count === 1 ? "1 Warnung" : `${count} Warnungen`),
|
|
394
|
+
openImage: ({ name }) => `Bild ${name} öffnen`,
|
|
380
395
|
steered: "Richtung geändert",
|
|
381
396
|
reasoning: "Überlegungen",
|
|
382
397
|
groupPlan: "Arbeitsplan aktualisiert",
|
|
@@ -575,6 +590,7 @@ const messages = i18n.define({
|
|
|
575
590
|
compactedCount: ({ count, date }) => `${count === 1 ? "1 Nachricht" : `${count} Nachrichten`} zusammengefasst · ${date}`,
|
|
576
591
|
compactedHint: "Ab hier sieht das Modell diese Zusammenfassung statt der Nachrichten darüber.",
|
|
577
592
|
compactedEmpty: "Kein sichtbarer Inhalt",
|
|
593
|
+
malformedImage: "Dieses Bild konnte nicht gelesen werden. Exportiere es erneut oder wähle eine andere Datei.",
|
|
578
594
|
},
|
|
579
595
|
},
|
|
580
596
|
});
|
|
@@ -20,7 +20,7 @@ import { type AiChatActions, AiChatActionsProvider, createAssistantMessageAction
|
|
|
20
20
|
import { isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
|
|
21
21
|
import { aiChatMessages } from "./messages";
|
|
22
22
|
import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
|
|
23
|
-
import { type AiTurnLayout, type AiTurnPhase, layoutAiTurn, waitsForUser } from "./turn-layout";
|
|
23
|
+
import { type AiTurnLayout, type AiTurnPhase, layoutAiTurn, storedPhase, waitsForUser } from "./turn-layout";
|
|
24
24
|
import { TurnNavigator } from "./turn-navigator";
|
|
25
25
|
import { activeTimelineSeq } from "./turn-navigator-utils";
|
|
26
26
|
import { type AiTurnDuration, type AiTurnSegment, AiTurnView } from "./turn-view";
|
|
@@ -102,13 +102,6 @@ const leadingSteerOpener = (blocks: readonly AssistantBlock[]): string | null =>
|
|
|
102
102
|
return opener;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
/** History phase of a finished loop, from how its loop ended. */
|
|
106
|
-
const storedPhase = (entries: readonly AiStoredMessage[]): AiTurnPhase => {
|
|
107
|
-
const reason = entries.findLast((entry) => entry.loopDoneReason)?.loopDoneReason;
|
|
108
|
-
if (!reason || reason === "stop") return "completed";
|
|
109
|
-
return reason === "aborted" ? "stopped" : "failed";
|
|
110
|
-
};
|
|
111
|
-
|
|
112
105
|
/** Responses with a work line, rich places, or a failure notice span the message column so their right edges align. */
|
|
113
106
|
const isWideLayout = (layout: AiTurnLayout, error?: AiTurnError) =>
|
|
114
107
|
layout.showWork || layout.results.length > 0 || layout.actions.length > 0 || Boolean(error);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* People-facing names of Cloud's built-in Assistant tools, keyed by tool name. Step rows, receipts, and approvals show
|
|
5
|
+
* these; a capability tool brings its own translated title instead. A tool test fails when a built-in tool has no entry.
|
|
6
|
+
*/
|
|
7
|
+
const toolNames = i18n.define({
|
|
8
|
+
baseLocale: "en",
|
|
9
|
+
messages: {
|
|
10
|
+
en: {
|
|
11
|
+
calculate: "Calculate",
|
|
12
|
+
card: "Card",
|
|
13
|
+
survey: "Survey",
|
|
14
|
+
text_editor: "Text editor",
|
|
15
|
+
local_bash: "Local Bash",
|
|
16
|
+
todo_write: "Working plan",
|
|
17
|
+
memory: "Memory",
|
|
18
|
+
list_files: "List files",
|
|
19
|
+
read_file: "Read file",
|
|
20
|
+
write_file: "Write file",
|
|
21
|
+
fetch_file: "Download file",
|
|
22
|
+
present: "Deliver file",
|
|
23
|
+
markdown_to_pdf: "Markdown to PDF",
|
|
24
|
+
html_to_pdf: "HTML to PDF",
|
|
25
|
+
view_image: "View image",
|
|
26
|
+
transcribe_audio: "Transcribe audio",
|
|
27
|
+
web_search: "Web search",
|
|
28
|
+
web_extract: "Read web page",
|
|
29
|
+
search_tools: "Search tools",
|
|
30
|
+
load_tools: "Load tools",
|
|
31
|
+
list_apps: "List apps",
|
|
32
|
+
search_help: "Search help",
|
|
33
|
+
read_help: "Read help",
|
|
34
|
+
search_project: "Search project",
|
|
35
|
+
read_project_knowledge: "Read project knowledge",
|
|
36
|
+
read_cloud_resource: "Read Cloud item",
|
|
37
|
+
load_skill: "Load skill",
|
|
38
|
+
search_skills: "Search skills",
|
|
39
|
+
code_run: "Run code",
|
|
40
|
+
code_action: "Run app action",
|
|
41
|
+
code_inspect: "Inspect run",
|
|
42
|
+
code_stop: "Stop run",
|
|
43
|
+
code_open: "Open app",
|
|
44
|
+
code_export: "Save run output",
|
|
45
|
+
code_present: "Show app in chat",
|
|
46
|
+
code_check: "App check",
|
|
47
|
+
code_secret: "Enter secret",
|
|
48
|
+
code_database: "App tables",
|
|
49
|
+
code_files: "List stored files",
|
|
50
|
+
code_file_stat: "File details",
|
|
51
|
+
code_file_copy: "Copy file",
|
|
52
|
+
code_database_export: "Export app database",
|
|
53
|
+
code_manage_read: "Read app details",
|
|
54
|
+
code_delete: "Delete app",
|
|
55
|
+
code_database_read: "Read app database",
|
|
56
|
+
code_database_clear: "Clear app data",
|
|
57
|
+
code_database_reset: "Reset app database",
|
|
58
|
+
code_storage_list: "List app storage",
|
|
59
|
+
code_storage_delete: "Delete from app storage",
|
|
60
|
+
code_access_read: "Read app access",
|
|
61
|
+
code_access_change: "Change app access",
|
|
62
|
+
code_actions: "List app actions",
|
|
63
|
+
code_sql: "Query app database",
|
|
64
|
+
code_versions: "List app versions",
|
|
65
|
+
code_list: "Find apps",
|
|
66
|
+
code_read: "Read app source",
|
|
67
|
+
code_history: "App history",
|
|
68
|
+
code_fork: "Copy app",
|
|
69
|
+
code_publish: "Publish app",
|
|
70
|
+
code_unpublish: "Withdraw app",
|
|
71
|
+
code_restore: "Restore app version",
|
|
72
|
+
code_update: "Update app details",
|
|
73
|
+
code_create: "Create app",
|
|
74
|
+
code_write: "Save app files",
|
|
75
|
+
code_remove: "Remove app file",
|
|
76
|
+
},
|
|
77
|
+
de: {
|
|
78
|
+
calculate: "Rechnen",
|
|
79
|
+
card: "Karte",
|
|
80
|
+
survey: "Umfrage",
|
|
81
|
+
text_editor: "Texteditor",
|
|
82
|
+
local_bash: "Lokale Bash",
|
|
83
|
+
todo_write: "Arbeitsplan",
|
|
84
|
+
memory: "Gedächtnis",
|
|
85
|
+
list_files: "Dateien auflisten",
|
|
86
|
+
read_file: "Datei lesen",
|
|
87
|
+
write_file: "Datei schreiben",
|
|
88
|
+
fetch_file: "Datei herunterladen",
|
|
89
|
+
present: "Datei bereitstellen",
|
|
90
|
+
markdown_to_pdf: "Markdown als PDF",
|
|
91
|
+
html_to_pdf: "HTML als PDF",
|
|
92
|
+
view_image: "Bild ansehen",
|
|
93
|
+
transcribe_audio: "Audio transkribieren",
|
|
94
|
+
web_search: "Websuche",
|
|
95
|
+
web_extract: "Webseite lesen",
|
|
96
|
+
search_tools: "Werkzeuge suchen",
|
|
97
|
+
load_tools: "Werkzeuge laden",
|
|
98
|
+
list_apps: "Apps auflisten",
|
|
99
|
+
search_help: "Hilfe durchsuchen",
|
|
100
|
+
read_help: "Hilfe lesen",
|
|
101
|
+
search_project: "Projekt durchsuchen",
|
|
102
|
+
read_project_knowledge: "Projektwissen lesen",
|
|
103
|
+
read_cloud_resource: "Cloud-Eintrag lesen",
|
|
104
|
+
load_skill: "Skill laden",
|
|
105
|
+
search_skills: "Skills suchen",
|
|
106
|
+
code_run: "Code ausführen",
|
|
107
|
+
code_action: "App-Aktion ausführen",
|
|
108
|
+
code_inspect: "Lauf prüfen",
|
|
109
|
+
code_stop: "Lauf beenden",
|
|
110
|
+
code_open: "App öffnen",
|
|
111
|
+
code_export: "Laufergebnis speichern",
|
|
112
|
+
code_present: "App im Chat zeigen",
|
|
113
|
+
code_check: "App-Prüfung",
|
|
114
|
+
code_secret: "Geheimnis eingeben",
|
|
115
|
+
code_database: "App-Tabellen",
|
|
116
|
+
code_files: "Gespeicherte Dateien auflisten",
|
|
117
|
+
code_file_stat: "Dateidetails",
|
|
118
|
+
code_file_copy: "Datei kopieren",
|
|
119
|
+
code_database_export: "App-Datenbank exportieren",
|
|
120
|
+
code_manage_read: "App-Details lesen",
|
|
121
|
+
code_delete: "App löschen",
|
|
122
|
+
code_database_read: "App-Datenbank lesen",
|
|
123
|
+
code_database_clear: "App-Daten leeren",
|
|
124
|
+
code_database_reset: "App-Datenbank zurücksetzen",
|
|
125
|
+
code_storage_list: "App-Speicher auflisten",
|
|
126
|
+
code_storage_delete: "Aus App-Speicher löschen",
|
|
127
|
+
code_access_read: "App-Zugriff lesen",
|
|
128
|
+
code_access_change: "App-Zugriff ändern",
|
|
129
|
+
code_actions: "App-Aktionen auflisten",
|
|
130
|
+
code_sql: "App-Datenbank abfragen",
|
|
131
|
+
code_versions: "App-Versionen auflisten",
|
|
132
|
+
code_list: "Apps suchen",
|
|
133
|
+
code_read: "App-Quelltext lesen",
|
|
134
|
+
code_history: "App-Verlauf",
|
|
135
|
+
code_fork: "App kopieren",
|
|
136
|
+
code_publish: "App veröffentlichen",
|
|
137
|
+
code_unpublish: "App zurückziehen",
|
|
138
|
+
code_restore: "App-Version wiederherstellen",
|
|
139
|
+
code_update: "App-Details ändern",
|
|
140
|
+
code_create: "App erstellen",
|
|
141
|
+
code_write: "App-Dateien speichern",
|
|
142
|
+
code_remove: "App-Datei entfernen",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** Older chats call the visual tools by their `cloud_` names. */
|
|
148
|
+
const ALIASES: Record<string, string> = { cloud_card: "card", cloud_survey: "survey", cloud_text_editor: "text_editor" };
|
|
149
|
+
|
|
150
|
+
/** The localized name of a built-in tool, or undefined for a tool Cloud does not ship. */
|
|
151
|
+
export const aiBuiltinToolName = (name: string, locale: string): string | undefined => {
|
|
152
|
+
const t: Record<string, string> = toolNames.resolve([locale]).t;
|
|
153
|
+
const key = ALIASES[name] ?? name;
|
|
154
|
+
return Object.hasOwn(t, key) ? t[key] : undefined;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const checkAiToolNames = () => toolNames.check();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AiTurnBlock, isRenderableTurnBlock } from "../protocol";
|
|
2
|
+
import type { AiStoredMessage } from "../types";
|
|
2
3
|
import { hasCapabilityTable } from "./capability-result";
|
|
3
4
|
import { isCardToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
4
5
|
import { isFailedTool } from "./tool-groups";
|
|
@@ -139,3 +140,14 @@ export function layoutAiTurn(input: readonly AiTurnBlock[], options: { phase: Ai
|
|
|
139
140
|
waitingFor,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
/** The loop ending is authoritative; partial messages also carry their own failed or interrupted ending. */
|
|
145
|
+
export const storedPhase = (entries: readonly Pick<AiStoredMessage, "loopDoneReason" | "stopReason">[]): AiTurnPhase => {
|
|
146
|
+
const reason = entries.findLast((entry) => entry.loopDoneReason)?.loopDoneReason;
|
|
147
|
+
if (reason === "aborted") return "stopped";
|
|
148
|
+
if (reason && reason !== "stop") return "failed";
|
|
149
|
+
const stop = entries.findLast((entry) => entry.stopReason)?.stopReason;
|
|
150
|
+
if (stop === "error") return "failed";
|
|
151
|
+
if (stop === "interrupted" || stop === "aborted") return "stopped";
|
|
152
|
+
return "completed";
|
|
153
|
+
};
|
|
@@ -127,7 +127,8 @@ const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean
|
|
|
127
127
|
if (["code_write", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
128
128
|
return withTarget(t.stepWritingCode);
|
|
129
129
|
if (["code_run", "code_action", "code_sql"].includes(name)) return withTarget(t.stepRunningCode);
|
|
130
|
-
if (name === "
|
|
130
|
+
if (name === "code_check") return t.stepCheckingApp;
|
|
131
|
+
if (name === "code_inspect") return t.stepCheckingRun;
|
|
131
132
|
if (name.startsWith("code_")) return t.stepCode;
|
|
132
133
|
if (["markdown_to_pdf", "html_to_pdf"].includes(name)) return withTarget(t.stepPdf);
|
|
133
134
|
if (["load_skill", "load_tools", "search_tools", "list_apps"].includes(name)) return t.stepLoading;
|
|
@@ -369,6 +370,7 @@ function ResultLinks(props: { links: Record<string, unknown>[] }) {
|
|
|
369
370
|
|
|
370
371
|
/** The summary and links of a table result, unless its receipt in place 4 already shows them. */
|
|
371
372
|
function CapabilityResultRow(props: { block: ToolBlock }) {
|
|
373
|
+
const locale = useLocale();
|
|
372
374
|
const block = () => props.block;
|
|
373
375
|
const summary = () => resultSummary(block());
|
|
374
376
|
const links = () => resultLinks(block());
|
|
@@ -377,7 +379,7 @@ function CapabilityResultRow(props: { block: ToolBlock }) {
|
|
|
377
379
|
<Chat.Activity
|
|
378
380
|
icon={aiToolIcon(block().name, block().presentation?.appIcon)}
|
|
379
381
|
accent={block().presentation?.appAccent}
|
|
380
|
-
label={summary() || (block().presentation?.title ?? displayToolName(block().name))}
|
|
382
|
+
label={summary() || (block().presentation?.title ?? displayToolName(block().name, locale()))}
|
|
381
383
|
trailing={links().length > 0 ? <ResultLinks links={links()} /> : undefined}
|
|
382
384
|
/>
|
|
383
385
|
</Show>
|
|
@@ -385,6 +387,7 @@ function CapabilityResultRow(props: { block: ToolBlock }) {
|
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; receipt: Accessor<boolean> }) {
|
|
390
|
+
const locale = useLocale();
|
|
388
391
|
const actions = useAiChatActions();
|
|
389
392
|
const block = () => props.result()?.block;
|
|
390
393
|
return (
|
|
@@ -414,7 +417,10 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
414
417
|
<Show when={!props.receipt()}>
|
|
415
418
|
<CapabilityResultRow block={current()} />
|
|
416
419
|
</Show>
|
|
417
|
-
<CapabilityTablePreview
|
|
420
|
+
<CapabilityTablePreview
|
|
421
|
+
result={current().result}
|
|
422
|
+
label={current().presentation?.title ?? displayToolName(current().name, locale())}
|
|
423
|
+
/>
|
|
418
424
|
</Match>
|
|
419
425
|
</Switch>
|
|
420
426
|
)}
|
|
@@ -426,7 +432,7 @@ function AiReceipt(props: { action: AiTurnAction; stopped: boolean }) {
|
|
|
426
432
|
const locale = useLocale();
|
|
427
433
|
const t = () => aiChatMessages(locale());
|
|
428
434
|
const block = () => props.action.block;
|
|
429
|
-
const title = () => block().presentation?.title ?? displayToolName(block().name);
|
|
435
|
+
const title = () => block().presentation?.title ?? displayToolName(block().name, locale());
|
|
430
436
|
const links = () => (props.action.state === "done" ? resultLinks(block()) : []);
|
|
431
437
|
const label = () => {
|
|
432
438
|
const state = props.action.state;
|
|
@@ -465,7 +471,9 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
465
471
|
const current = action();
|
|
466
472
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
467
473
|
announce(
|
|
468
|
-
aiChatMessages(locale()).approvalNeeded({
|
|
474
|
+
aiChatMessages(locale()).approvalNeeded({
|
|
475
|
+
title: current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
476
|
+
}),
|
|
469
477
|
);
|
|
470
478
|
}),
|
|
471
479
|
);
|
|
@@ -500,7 +508,7 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
500
508
|
<Switch
|
|
501
509
|
fallback={
|
|
502
510
|
<Chat.Activity
|
|
503
|
-
label={current().block.presentation?.title ?? displayToolName(current().block.name)}
|
|
511
|
+
label={current().block.presentation?.title ?? displayToolName(current().block.name, locale())}
|
|
504
512
|
icon={aiToolIcon(current().block.name)}
|
|
505
513
|
busy={current().block.status === "awaiting_client"}
|
|
506
514
|
/>
|
|
@@ -516,7 +524,7 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
516
524
|
</Match>
|
|
517
525
|
<Match when={current().state === "running"}>
|
|
518
526
|
<Chat.Activity
|
|
519
|
-
label={current().block.presentation?.title ?? displayToolName(current().block.name)}
|
|
527
|
+
label={current().block.presentation?.title ?? displayToolName(current().block.name, locale())}
|
|
520
528
|
icon={aiToolIcon(current().block.name, current().block.presentation?.appIcon)}
|
|
521
529
|
accent={current().block.presentation?.appAccent}
|
|
522
530
|
busy
|
package/src/ai/chat-tasks.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getCapability } from "../_internal/registry";
|
|
|
4
4
|
import { logger } from "../services/logging";
|
|
5
5
|
import {
|
|
6
6
|
createMandate,
|
|
7
|
+
MANDATE_POLICY_DENIED_MESSAGE,
|
|
7
8
|
type Mandate,
|
|
8
9
|
type MandatePolicyV1,
|
|
9
10
|
MandatePolicyV1Schema,
|
|
@@ -88,7 +89,14 @@ export class AiChatTaskIdempotencyConflictError extends Error {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
class AiChatTaskCreateRace extends Error {}
|
|
91
|
-
class AiChatTaskAuthorityError extends Error {
|
|
92
|
+
export class AiChatTaskAuthorityError extends Error {
|
|
93
|
+
constructor(
|
|
94
|
+
message: string,
|
|
95
|
+
readonly code?: string,
|
|
96
|
+
) {
|
|
97
|
+
super(message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
92
100
|
const log = logger("ai:chat-tasks");
|
|
93
101
|
|
|
94
102
|
type OccurrenceRow = {
|
|
@@ -497,10 +505,15 @@ export const aiChatTasks = {
|
|
|
497
505
|
input: input.input,
|
|
498
506
|
capabilityApproval: input.approval,
|
|
499
507
|
});
|
|
500
|
-
if (!result.ok)
|
|
508
|
+
if (!result.ok) {
|
|
509
|
+
const policyDenied = result.error.code === "FORBIDDEN" && result.error.message === MANDATE_POLICY_DENIED_MESSAGE;
|
|
501
510
|
throw new AiChatTaskAuthorityError(
|
|
502
|
-
|
|
511
|
+
policyDenied
|
|
512
|
+
? `This task's grants do not allow ${input.appId}.${input.capabilityId} with these inputs; continue with the granted tools or explain what is missing.`
|
|
513
|
+
: `Background task cannot call ${input.appId}.${input.capabilityId}: ${result.error.message}. Check the task grants and fixed inputs in the normal chat; this run cannot expand them.`,
|
|
514
|
+
policyDenied ? "MANDATE_POLICY_DENIED" : result.error.code,
|
|
503
515
|
);
|
|
516
|
+
}
|
|
504
517
|
},
|
|
505
518
|
list: async (input: {
|
|
506
519
|
userId: string;
|