@k2b/cloud 0.6.0 → 0.7.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/README.md +2 -2
- package/package.json +19 -9
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatModelOption, ChatSubmitInput } from "@k2b/ui";
|
|
1
|
+
import type { ChatAttachment, ChatMention, ChatModelOption, ChatSubmitInput } from "@k2b/ui";
|
|
2
2
|
import { AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "../limits";
|
|
3
3
|
import type { AiResourceMarker } from "../resource-markers";
|
|
4
|
-
import type { AiPublicModelProfile, AiUserContentPart } from "../types";
|
|
4
|
+
import type { AiDraftContentPart, AiPublicModelProfile, AiUserContentPart } from "../types";
|
|
5
5
|
import {
|
|
6
6
|
type AiComposerAttachment,
|
|
7
7
|
FILE_INPUT_ACCEPT,
|
|
@@ -21,6 +21,7 @@ const AI_COMPOSER_TEXT_MAX_BYTES = AI_COMPOSER_TEXT_MAX_CHARS * 4;
|
|
|
21
21
|
const AI_PASTED_TEXT_FILE_NAME = /^pasted-(?:text|[a-f0-9]{8})(?:-\d+)?\.txt$/i;
|
|
22
22
|
|
|
23
23
|
export type AiComposerSendInput = {
|
|
24
|
+
draftContent?: AiDraftContentPart[];
|
|
24
25
|
message?: string;
|
|
25
26
|
content?: AiUserContentPart[];
|
|
26
27
|
files?: File[];
|
|
@@ -96,7 +97,7 @@ const isAiComposerAttachment = (value: unknown): value is AiComposerAttachment =
|
|
|
96
97
|
if (!value || typeof value !== "object") return false;
|
|
97
98
|
const candidate = value as Partial<AiComposerAttachment>;
|
|
98
99
|
return (
|
|
99
|
-
["image", "file", "stored-file", "resource"].includes(candidate.kind ?? "") &&
|
|
100
|
+
["image", "file", "stored-file", "resource", "project-file"].includes(candidate.kind ?? "") &&
|
|
100
101
|
typeof candidate.id === "string" &&
|
|
101
102
|
typeof candidate.name === "string"
|
|
102
103
|
);
|
|
@@ -107,6 +108,24 @@ export const aiComposerAttachmentRecords = (attachments: readonly ChatAttachment
|
|
|
107
108
|
|
|
108
109
|
export const aiComposerSendInput = (input: ChatSubmitInput): AiComposerSendInput => {
|
|
109
110
|
const attachments = aiComposerAttachmentRecords(input.attachments);
|
|
111
|
+
if (input.mentions?.length || attachments.some(attachment => attachment.kind === "project-file")) {
|
|
112
|
+
const draftContent: AiDraftContentPart[] = [];
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const mention of [...(input.mentions ?? [])].sort((a,b) => a.start - b.start)) {
|
|
115
|
+
if (mention.start < offset || mention.end > input.text.length || mention.start >= mention.end) continue;
|
|
116
|
+
const attachment = aiComposerAttachmentRecords([mention.attachment])[0];
|
|
117
|
+
if (!attachment || attachment.kind === "file" || attachment.kind === "image") continue;
|
|
118
|
+
if (mention.start > offset) draftContent.push({ type: "text", text: input.text.slice(offset, mention.start) });
|
|
119
|
+
draftContent.push(composerAttachmentPart(attachment, true));
|
|
120
|
+
offset = mention.end;
|
|
121
|
+
}
|
|
122
|
+
if (offset < input.text.length) draftContent.push({ type: "text", text: input.text.slice(offset) });
|
|
123
|
+
for (const attachment of attachments) {
|
|
124
|
+
if (attachment.kind !== "file" && attachment.kind !== "image") draftContent.push(composerAttachmentPart(attachment));
|
|
125
|
+
}
|
|
126
|
+
const files = attachments.flatMap(attachment => attachment.kind === "file" || attachment.kind === "image" ? [attachment.file] : []);
|
|
127
|
+
return { draftContent, files: files.length ? files : undefined };
|
|
128
|
+
}
|
|
110
129
|
const files = attachments.filter(
|
|
111
130
|
(attachment): attachment is PendingAiImage | PendingAiVfsFile => attachment.kind === "image" || attachment.kind === "file",
|
|
112
131
|
);
|
|
@@ -167,3 +186,29 @@ export const readAiComposerFiles = async (
|
|
|
167
186
|
discarded: Math.max(0, files.length - candidates.length),
|
|
168
187
|
};
|
|
169
188
|
};
|
|
189
|
+
|
|
190
|
+
const composerAttachmentPart = (attachment: Exclude<AiComposerAttachment, {kind: "image" | "file"}>, inline = false): AiDraftContentPart => {
|
|
191
|
+
if (attachment.kind === "resource") return { type: "resource", ref: attachment.ref, title: attachment.name, icon: attachment.icon, href: attachment.href, inline };
|
|
192
|
+
if (attachment.kind === "project-file") return { type: "project-file", path: attachment.path, inline };
|
|
193
|
+
return { type: "file", path: attachment.path, version: attachment.version, mediaType: attachment.mediaType, size: attachment.size, inline };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** Hydrate the same ordered content used by the server draft and queued messages. */
|
|
197
|
+
export function aiComposerDraft(content: readonly AiDraftContentPart[]): { text: string; mentions: ChatMention[]; attachments: AiComposerAttachment[] } {
|
|
198
|
+
let text = "";
|
|
199
|
+
const mentions: ChatMention[] = [];
|
|
200
|
+
const attachments: AiComposerAttachment[] = [];
|
|
201
|
+
for (const part of content) {
|
|
202
|
+
if (part.type === "text") { text += part.text; continue; }
|
|
203
|
+
const attachment: AiComposerAttachment = part.type === "resource"
|
|
204
|
+
? { kind: "resource", id: `resource:${part.ref.type}:${part.ref.id}`, ref: part.ref, name: part.title ?? part.ref.id, icon: part.icon ?? "ti ti-link", href: part.href }
|
|
205
|
+
: part.type === "project-file"
|
|
206
|
+
? { kind: "project-file", id: part.path, path: part.path, name: part.path, icon: "ti ti-file" }
|
|
207
|
+
: { kind: "stored-file", id: `file:${part.path}:${part.version}`, path: part.path, name: part.path.split("/").at(-1) || part.path, version: part.version, mediaType: part.mediaType, size: part.size, icon: "ti ti-file" };
|
|
208
|
+
if (part.inline) {
|
|
209
|
+
mentions.push({ start: text.length, end: text.length + attachment.name.length, attachment: aiChatAttachments([attachment])[0]! });
|
|
210
|
+
text += attachment.name;
|
|
211
|
+
} else attachments.push(attachment);
|
|
212
|
+
}
|
|
213
|
+
return { text, mentions, attachments };
|
|
214
|
+
}
|
|
@@ -28,6 +28,8 @@ type RetryMessageHandler = (entry: AiStoredMessage, input?: AiRetryMessageInput)
|
|
|
28
28
|
type RetrySteerHandler = (block: Extract<AiTurnBlock, { kind: "steer_message" }>) => void | Promise<void>;
|
|
29
29
|
|
|
30
30
|
export type AiChatActions = {
|
|
31
|
+
/** Host-owned visualizations, persisted independently of the execution session. */
|
|
32
|
+
renderCodePresentation?: (result: unknown) => JSX.Element;
|
|
31
33
|
/** Prevents turn-continuation actions while the current turn is stopping. */
|
|
32
34
|
actionDisabled?: () => boolean;
|
|
33
35
|
onApproval?: ApprovalHandler;
|
|
@@ -36,8 +38,12 @@ export type AiChatActions = {
|
|
|
36
38
|
onRetryMessage?: RetryMessageHandler;
|
|
37
39
|
onRetrySteer?: RetrySteerHandler;
|
|
38
40
|
onMessageFeedback?: (entry: AiStoredMessage, feedback: Omit<AiMessageFeedback, "updatedAt"> | null) => void | Promise<void>;
|
|
41
|
+
/** Open the originating background run for a delivered scheduled result. */
|
|
42
|
+
onOpenScheduledTaskRun?: (taskId: string, occurrenceId: string) => void;
|
|
39
43
|
/** Open a conversation VFS file in the host application's artifact surface. */
|
|
40
44
|
onOpenFile?: (path: string) => void;
|
|
45
|
+
/** Resolve a Markdown link only when it identifies a known conversation file. */
|
|
46
|
+
resolveFileLink?: (href: string) => { path: string; href: string } | null;
|
|
41
47
|
/** Download URL for a conversation VFS file (present blocks, attachment chips). */
|
|
42
48
|
fileUrl?: (path: string) => string | null;
|
|
43
49
|
};
|
|
@@ -53,8 +59,8 @@ export function AiChatActionsProvider(props: { actions?: AiChatActions; children
|
|
|
53
59
|
return <AiChatActionContext.Provider value={props.actions ?? {}}>{props.children}</AiChatActionContext.Provider>;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
const usageValue = (usage: Usage | null | undefined, key: "input" | "output" | "total"
|
|
57
|
-
const value = (usage as Partial<Record<"input" | "output" | "total"
|
|
62
|
+
const usageValue = (usage: Usage | null | undefined, key: "input" | "output" | "total") => {
|
|
63
|
+
const value = (usage as Partial<Record<"input" | "output" | "total", unknown>> | null | undefined)?.[key];
|
|
58
64
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
59
65
|
};
|
|
60
66
|
|
|
@@ -70,7 +76,6 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
70
76
|
const toolCalls = blocks.filter((block) => block.type === "tool_call");
|
|
71
77
|
const aggregate = entry.loopAggregate;
|
|
72
78
|
const usage = aggregate?.usage ?? entry.usage;
|
|
73
|
-
const credits = usageValue(usage, "creditsUsed");
|
|
74
79
|
const timing = aggregate?.timing;
|
|
75
80
|
|
|
76
81
|
const meta = [
|
|
@@ -100,7 +105,6 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
100
105
|
input: usageValue(usage, "input"),
|
|
101
106
|
output: usageValue(usage, "output"),
|
|
102
107
|
total: usageValue(usage, "total"),
|
|
103
|
-
credits,
|
|
104
108
|
},
|
|
105
109
|
turns: aggregate?.assistantMessageCount ?? 1,
|
|
106
110
|
toolCallCount: aggregate?.toolCallCount ?? toolCalls.length,
|
|
@@ -184,9 +188,6 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
184
188
|
accent={{ tone: "red", icon: "ti ti-alert-triangle" }}
|
|
185
189
|
/>
|
|
186
190
|
</Show>
|
|
187
|
-
<Show when={info.usage.credits !== null && info.usage.credits > 0}>
|
|
188
|
-
<StatCell label="Credits" value={info.usage.credits?.toLocaleString(locale, { maximumFractionDigits: 6 }) ?? "–"} />
|
|
189
|
-
</Show>
|
|
190
191
|
</StatGrid>
|
|
191
192
|
|
|
192
193
|
<Show when={info.tools.length > 0}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AI_AUDIO_EXTENSIONS } from "../audio-format";
|
|
1
2
|
import type { Message, Usage } from "@k2b/nessi";
|
|
2
3
|
import { fileIcons } from "@k2b/stdlib";
|
|
3
4
|
import { formatBytes as sharedFormatBytes } from "../../shared/format";
|
|
@@ -20,6 +21,7 @@ export type AiForkMessageInput = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export type AiComposerAttachment =
|
|
24
|
+
| { kind: "project-file"; id: string; name: string; path: string; icon: string }
|
|
23
25
|
| {
|
|
24
26
|
kind: "image";
|
|
25
27
|
id: string;
|
|
@@ -111,6 +113,8 @@ export const TEXT_ATTACHMENT_MEDIA_TYPES = new Set([
|
|
|
111
113
|
]);
|
|
112
114
|
export const FILE_INPUT_ACCEPT = [
|
|
113
115
|
...AI_IMAGE_MEDIA_TYPES,
|
|
116
|
+
"audio/*",
|
|
117
|
+
...AI_AUDIO_EXTENSIONS.map((extension) => `.${extension}`),
|
|
114
118
|
"text/*",
|
|
115
119
|
...Array.from(TEXT_ATTACHMENT_MEDIA_TYPES),
|
|
116
120
|
...Array.from(new Set([...TEXT_ATTACHMENT_EXTENSIONS, ...DOCUMENT_ATTACHMENT_EXTENSIONS]), (extension) => `.${extension}`),
|
|
@@ -228,14 +232,20 @@ export type AiLatestUsageSnapshot = {
|
|
|
228
232
|
export const latestUsageSnapshot = (messages: AiStoredMessage[]): AiLatestUsageSnapshot | null => {
|
|
229
233
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
230
234
|
const entry = messages[i];
|
|
235
|
+
if (entry?.message.role !== "assistant") continue;
|
|
231
236
|
const request = entry?.loopAggregate?.turns.findLast((turn) => Boolean(turn.usage))?.usage ?? entry?.usage;
|
|
232
237
|
if (request) {
|
|
233
238
|
return {
|
|
234
239
|
request,
|
|
235
|
-
loop: entry?.loopAggregate?.usage ?? entry?.
|
|
240
|
+
loop: entry?.loopAggregate?.usage ?? (entry?.loopId ? messages.filter(message => message.loopId === entry.loopId && message.message.role === "assistant").reduce<Usage>((sum, message) => ({
|
|
241
|
+
input: sum.input + (message.usage?.input ?? 0), output: sum.output + (message.usage?.output ?? 0), total: sum.total + (message.usage?.total ?? 0),
|
|
242
|
+
...((sum.cacheRead !== undefined || message.usage?.cacheRead !== undefined) ? { cacheRead: (sum.cacheRead ?? 0) + (message.usage?.cacheRead ?? 0) } : {}),
|
|
243
|
+
}), { input: 0, output: 0, total: 0 }) : request),
|
|
236
244
|
modelProfileId: entry?.modelProfileId ?? null,
|
|
237
245
|
};
|
|
238
246
|
}
|
|
247
|
+
// A provider without usage must not inherit a previous request's numbers.
|
|
248
|
+
return null;
|
|
239
249
|
}
|
|
240
250
|
return null;
|
|
241
251
|
};
|
|
@@ -294,6 +304,7 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
|
|
|
294
304
|
["search_tools", "ti ti-ai-gateway"],
|
|
295
305
|
["load_tools", "ti ti-ai-gateway"],
|
|
296
306
|
["load_skill", "ti ti-sparkles"],
|
|
307
|
+
["todo_write", "ti ti-list-check"],
|
|
297
308
|
["list_apps", "ti ti-apps"],
|
|
298
309
|
["read_cloud_resource", "ti ti-ai-gateway"],
|
|
299
310
|
]);
|
package/src/ai/chat/messages.ts
CHANGED
|
@@ -4,6 +4,7 @@ const messages = i18n.define({
|
|
|
4
4
|
baseLocale: "en",
|
|
5
5
|
messages: {
|
|
6
6
|
en: {
|
|
7
|
+
backgroundRun: "Background run",
|
|
7
8
|
tableLinks: "Links",
|
|
8
9
|
tableOpen: "Open",
|
|
9
10
|
tableEmpty: "No matching rows.",
|
|
@@ -20,6 +21,7 @@ const messages = i18n.define({
|
|
|
20
21
|
byteRange: ({ start, end }: { start: string; end: string }) => `Bytes ${start}–${end}`,
|
|
21
22
|
},
|
|
22
23
|
de: {
|
|
24
|
+
backgroundRun: "Hintergrundlauf",
|
|
23
25
|
tableLinks: "Links",
|
|
24
26
|
tableOpen: "Öffnen",
|
|
25
27
|
tableEmpty: "Keine passenden Zeilen.",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Chat, type ChatTimelineItem, useLocale } from "@k2b/ui";
|
|
1
|
+
import { Button, Chat, type ChatTimelineItem, useLocale } from "@k2b/ui";
|
|
2
2
|
import { type Accessor, createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from "solid-js";
|
|
3
3
|
import type { AiActiveTurn } from "../client/projection";
|
|
4
4
|
import { type AiActiveTurnSegment, isRenderableTurnBlock, splitActiveTurnBlocks } from "../protocol";
|
|
@@ -6,7 +6,7 @@ import { type AiAssistantTimelineItem, buildAiMessageTimeline, copyTextFromAssis
|
|
|
6
6
|
import type { AiConversationTimelineEntry, AiStoredMessage } from "../types";
|
|
7
7
|
import { AiTurnBlockList } from "./blocks";
|
|
8
8
|
import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
|
|
9
|
-
import { formatWorkedDuration,
|
|
9
|
+
import { formatWorkedDuration, isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
|
|
10
10
|
import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
|
|
11
11
|
import { aiChatMessages } from "./messages";
|
|
12
12
|
import { CloudSurveyResultBlock } from "./visual-tools";
|
|
@@ -63,53 +63,17 @@ const surveyItem = (block: SurveyResultBlock, turnId: string): ChatTimelineItem
|
|
|
63
63
|
content: <CloudSurveyResultBlock args={block.args} result={block.result} />,
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
const isDirectCompletedResult = (block: AssistantBlock): boolean =>
|
|
67
|
-
block.kind === "tool" && block.status === "completed" && !block.isError && (isCardToolName(block.name) || block.name === "present");
|
|
68
|
-
|
|
69
|
-
const isFailedBlock = (block: AssistantBlock): boolean =>
|
|
70
|
-
(block.kind === "tool" && (block.isError || block.status === "failed" || block.status === "rejected")) ||
|
|
71
|
-
(block.kind === "compaction" && block.status === "failed");
|
|
72
|
-
|
|
73
|
-
export const partitionCompletedAssistantBlocks = (blocks: AssistantBlock[]) => {
|
|
74
|
-
const renderable = blocks.filter(isRenderableTurnBlock);
|
|
75
|
-
const finalTextIndex = renderable.findLastIndex((block) => block.kind === "text");
|
|
76
|
-
return {
|
|
77
|
-
worked: renderable.filter((block, index) => index !== finalTextIndex && !isDirectCompletedResult(block)),
|
|
78
|
-
visible: renderable.filter((block, index) => index === finalTextIndex || isDirectCompletedResult(block)),
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
|
|
82
66
|
export function AiAssistantContent(props: {
|
|
83
67
|
item: AiAssistantTimelineItem;
|
|
84
68
|
disclosureState?: AiToolDisclosureState;
|
|
85
69
|
segmentId?: string;
|
|
86
70
|
}): JSX.Element {
|
|
87
71
|
const locale = useLocale();
|
|
88
|
-
const blocks = createMemo(() => partitionCompletedAssistantBlocks(props.item.blocks));
|
|
89
72
|
const turnId = () => props.item.loopId ?? props.item.id;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<div class="flex flex-col gap-2">
|
|
97
|
-
<Show when={blocks().worked.length > 0}>
|
|
98
|
-
<Chat.Activity
|
|
99
|
-
icon="ti ti-route"
|
|
100
|
-
label={props.segmentId ? aiChatMessages(locale()).workSteps : `Worked for ${formatWorkedDuration(props.item.workedMs)}`}
|
|
101
|
-
tone={workedFailed() ? "danger" : undefined}
|
|
102
|
-
bodyInset={false}
|
|
103
|
-
defaultOpen={workedFailed()}
|
|
104
|
-
open={workedOpen()}
|
|
105
|
-
onOpenChange={setWorkedOpen}
|
|
106
|
-
>
|
|
107
|
-
<AiTurnBlockList blocks={blocks().worked} turnId={turnId()} compact disclosureState={props.disclosureState} />
|
|
108
|
-
</Chat.Activity>
|
|
109
|
-
</Show>
|
|
110
|
-
<AiTurnBlockList blocks={blocks().visible} turnId={turnId()} disclosureState={props.disclosureState} />
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
73
|
+
return <div class="flex flex-col gap-2">
|
|
74
|
+
<Chat.Activity icon="ti ti-route" label={`${locale().startsWith("de") ? "Gearbeitet für" : "Worked for"} ${formatWorkedDuration(props.item.workedMs)}`} />
|
|
75
|
+
<AiTurnBlockList blocks={props.item.blocks} turnId={turnId()} disclosureState={props.disclosureState} />
|
|
76
|
+
</div>;
|
|
113
77
|
}
|
|
114
78
|
|
|
115
79
|
const storedItems = (
|
|
@@ -206,11 +170,18 @@ const storedItems = (
|
|
|
206
170
|
createdAt: actionEntry?.createdAt ?? item.entries.at(-1)?.createdAt,
|
|
207
171
|
class: segment.blocks.some(isWideBlock) ? "ai-chat-message-wide" : undefined,
|
|
208
172
|
content: (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
173
|
+
<>
|
|
174
|
+
<Show when={index === 0 && item.entries.find(entry => entry.meta?.scheduledTask)?.meta?.scheduledTask}>
|
|
175
|
+
{task => <Button variant="ghost" size="xs" class="mb-2" onClick={() => actions.onOpenScheduledTaskRun?.(task().taskId, task().occurrenceId)} disabled={!actions.onOpenScheduledTaskRun}>
|
|
176
|
+
<i class="ti ti-calendar-time" aria-hidden="true" /> {aiChatMessages(locale).backgroundRun}
|
|
177
|
+
</Button>}
|
|
178
|
+
</Show>
|
|
179
|
+
<AiAssistantContent
|
|
180
|
+
item={{ ...item, blocks: segment.blocks }}
|
|
181
|
+
segmentId={segments.length > 1 ? `${item.id}:${index}` : undefined}
|
|
182
|
+
disclosureState={disclosureState}
|
|
183
|
+
/>
|
|
184
|
+
</>
|
|
214
185
|
),
|
|
215
186
|
actions:
|
|
216
187
|
actionEntry && index === lastAssistant
|
|
@@ -268,7 +239,7 @@ const activeItems = (turn: AiActiveTurn | null, actions: AiChatActions, disclosu
|
|
|
268
239
|
blocks={blocks}
|
|
269
240
|
turnId={turn.turnId}
|
|
270
241
|
streaming={turn.status === "running" && index === segments.length - 1}
|
|
271
|
-
active
|
|
242
|
+
active={index === segments.length - 1}
|
|
272
243
|
disclosureState={disclosureState}
|
|
273
244
|
/>
|
|
274
245
|
),
|
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
import { createEffect, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { useAiChatActions } from "./message-actions";
|
|
3
|
+
|
|
1
4
|
export function AssistantMarkdownBlock(props: { html: string }) {
|
|
2
|
-
|
|
5
|
+
const actions = useAiChatActions();
|
|
6
|
+
let element!: HTMLDivElement;
|
|
7
|
+
const originals = new WeakMap<HTMLAnchorElement, { href: string; target: string | null }>();
|
|
8
|
+
const files = new WeakMap<HTMLAnchorElement, string>();
|
|
9
|
+
createEffect(() => {
|
|
10
|
+
props.html;
|
|
11
|
+
for (const link of Array.from(element.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
|
|
12
|
+
const original = originals.get(link) ?? { href: link.getAttribute("href")!, target: link.getAttribute("target") };
|
|
13
|
+
originals.set(link, original);
|
|
14
|
+
const file = actions.resolveFileLink?.(original.href);
|
|
15
|
+
link.setAttribute("href", file?.href ?? original.href);
|
|
16
|
+
const target = file ? null : original.target;
|
|
17
|
+
if (target) link.setAttribute("target", target);
|
|
18
|
+
else link.removeAttribute("target");
|
|
19
|
+
if (file) files.set(link, file.path);
|
|
20
|
+
else files.delete(link);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
onMount(() => {
|
|
24
|
+
const handleClick = (event: MouseEvent) => {
|
|
25
|
+
if (
|
|
26
|
+
event.defaultPrevented ||
|
|
27
|
+
event.button !== 0 ||
|
|
28
|
+
event.metaKey ||
|
|
29
|
+
event.ctrlKey ||
|
|
30
|
+
event.shiftKey ||
|
|
31
|
+
event.altKey ||
|
|
32
|
+
!actions.onOpenFile
|
|
33
|
+
)
|
|
34
|
+
return;
|
|
35
|
+
const link = event.target instanceof Element ? event.target.closest("a") : null;
|
|
36
|
+
const path = link ? files.get(link) : undefined;
|
|
37
|
+
if (!path) return;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
actions.onOpenFile(path);
|
|
40
|
+
};
|
|
41
|
+
element.addEventListener("click", handleClick);
|
|
42
|
+
onCleanup(() => element.removeEventListener("click", handleClick));
|
|
43
|
+
});
|
|
44
|
+
return <div ref={element} class="assistant-markdown-block" innerHTML={props.html} />;
|
|
3
45
|
}
|
|
@@ -8,9 +8,24 @@ export type AiToolDisclosureState = {
|
|
|
8
8
|
|
|
9
9
|
export const createAiToolDisclosureState = (): AiToolDisclosureState => {
|
|
10
10
|
const openByBlockId = new Map<string, boolean>();
|
|
11
|
+
const storageKey = (blockId: string) => `cloud.ai.tool-disclosure:${blockId}`;
|
|
11
12
|
return {
|
|
12
|
-
get: (blockId) =>
|
|
13
|
-
|
|
13
|
+
get: (blockId) => {
|
|
14
|
+
if (!openByBlockId.has(blockId)) {
|
|
15
|
+
try {
|
|
16
|
+
const saved = window.sessionStorage.getItem(storageKey(blockId));
|
|
17
|
+
if (saved === "open") openByBlockId.set(blockId, true);
|
|
18
|
+
} catch { /* SSR and disabled storage keep the in-memory state. */ }
|
|
19
|
+
}
|
|
20
|
+
return openByBlockId.get(blockId);
|
|
21
|
+
},
|
|
22
|
+
set: (blockId, open) => {
|
|
23
|
+
openByBlockId.set(blockId, open);
|
|
24
|
+
try {
|
|
25
|
+
if (open) window.sessionStorage.setItem(storageKey(blockId), "open");
|
|
26
|
+
else window.sessionStorage.removeItem(storageKey(blockId));
|
|
27
|
+
} catch { /* Storage limits must not prevent expanding a tool. */ }
|
|
28
|
+
},
|
|
14
29
|
};
|
|
15
30
|
};
|
|
16
31
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hasCapabilityTable } from "./capability-result";
|
|
2
|
+
import type { AiTurnBlock } from "../protocol";
|
|
3
|
+
import { isCardToolName, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
4
|
+
|
|
5
|
+
type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
6
|
+
type Group = { kind: "tools"; blocks: Tool[] } | { kind: "block"; block: AiTurnBlock };
|
|
7
|
+
|
|
8
|
+
export function isFailedTool(tool: Tool): boolean {
|
|
9
|
+
if (tool.isError || tool.status === "failed" || tool.status === "rejected") return true;
|
|
10
|
+
const result = tool.result;
|
|
11
|
+
return ["code_run","code_inspect","code_interact"].includes(tool.name) && result !== null && typeof result === "object" && "status" in result && result.status === "error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Visible content and interactive decisions are boundaries; model request boundaries are not. */
|
|
15
|
+
export function groupToolBlocks(blocks: readonly AiTurnBlock[]): Group[] {
|
|
16
|
+
const groups: Group[] = [];
|
|
17
|
+
for (const block of blocks) {
|
|
18
|
+
const ordinary =
|
|
19
|
+
block.kind === "tool" &&
|
|
20
|
+
!block.approval &&
|
|
21
|
+
block.status !== "awaiting_approval" &&
|
|
22
|
+
!["present", "code_present", "view_image", "code_secret"].includes(block.name) &&
|
|
23
|
+
!isCardToolName(block.name) &&
|
|
24
|
+
!isSurveyToolName(block.name) &&
|
|
25
|
+
!isTextEditorToolName(block.name) &&
|
|
26
|
+
!hasCapabilityTable(block);
|
|
27
|
+
if (ordinary) {
|
|
28
|
+
const last = groups.at(-1);
|
|
29
|
+
if (last?.kind === "tools") last.blocks.push(block);
|
|
30
|
+
else groups.push({ kind: "tools", blocks: [block] });
|
|
31
|
+
} else groups.push({ kind: "block", block });
|
|
32
|
+
}
|
|
33
|
+
return groups;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function summarizeToolGroup(tools: readonly Tool[], locale: string): string {
|
|
37
|
+
const de = locale.startsWith("de");
|
|
38
|
+
const categories = tools.map((tool) => {
|
|
39
|
+
if (tool.name === "todo_write") return de ? "Arbeitsplan aktualisiert" : "Updated working plan";
|
|
40
|
+
if (tool.name.startsWith("code_")) return de ? "Mit Code gearbeitet" : "Worked with code";
|
|
41
|
+
if (tool.presentation?.kind === "capability" || tool.name === "read_cloud_resource")
|
|
42
|
+
return de ? "Cloud-Integration verwendet" : "Used Cloud integration";
|
|
43
|
+
if (["read_file", "list_files", "view_image"].includes(tool.name)) return de ? "Dateien gelesen" : "Read files";
|
|
44
|
+
if (tool.name === "write_file") return de ? "Dateien geschrieben" : "Wrote files";
|
|
45
|
+
if (["load_skill", "load_tools", "search_tools"].includes(tool.name))
|
|
46
|
+
return de ? "Werkzeuge und Wissen geladen" : "Loaded tools and guidance";
|
|
47
|
+
if (tool.name.startsWith("web_") || tool.name === "fetch_file") return de ? "Im Web recherchiert" : "Searched the web";
|
|
48
|
+
return de ? "Werkzeuge verwendet" : "Used tools";
|
|
49
|
+
});
|
|
50
|
+
const errors = tools.filter(isFailedTool).length;
|
|
51
|
+
return [...new Set(categories)].join(", ") + (errors ? ` · ${errors} ${de ? "fehlgeschlagen" : "failed"}` : "");
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { hasBillableAiPricing } from "../shared/ai-costs";
|
|
2
|
+
import { readAiSettingsState } from "./settings";
|
|
3
|
+
import type { AccessSubject } from "../server/services/access";
|
|
4
|
+
import type { AiChatQuotaSnapshot } from "../shared/ai-quotas";
|
|
5
|
+
import { listAssistantAiModels } from "./assistant-models";
|
|
6
|
+
import { aiQuotas } from "./quotas";
|
|
7
|
+
|
|
8
|
+
/** Own allowances only. Grant identities and unavailable model profiles stay private. */
|
|
9
|
+
export async function getAiChatQuotas(subject: AccessSubject): Promise<AiChatQuotaSnapshot> {
|
|
10
|
+
const config = await aiQuotas.config();
|
|
11
|
+
if (!config.enabled) return { enabled: false, balances: [] };
|
|
12
|
+
const models = await listAssistantAiModels(subject);
|
|
13
|
+
const snapshot = await aiQuotas.snapshot(subject);
|
|
14
|
+
const profiles = (await readAiSettingsState()).profiles;
|
|
15
|
+
return {
|
|
16
|
+
enabled: snapshot.enabled,
|
|
17
|
+
unit: config.unit ?? "EUR",
|
|
18
|
+
unlimitedModels: models
|
|
19
|
+
.filter((model) => !hasBillableAiPricing(profiles.find((profile) => profile.id === model.id)?.pricing))
|
|
20
|
+
.map((model) => model.id),
|
|
21
|
+
balances: snapshot.balances
|
|
22
|
+
.filter((b) => b.scope === "*" || models.some((m) => m.id === b.scope))
|
|
23
|
+
.map(({ sources: _sources, sourceDetails: _sourceDetails, ...balance }) => balance),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CapabilityGrantsSchema, type CapabilityGrant } from "../services/mandates";
|
|
2
|
+
export { CapabilityGrantsSchema as ChatTaskGrantsSchema };
|
|
1
3
|
import { dates } from "@k2b/stdlib";
|
|
2
4
|
import {
|
|
3
5
|
type AiChatTask,
|
|
@@ -16,9 +18,11 @@ export const ChatTaskOccurrenceIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).
|
|
|
16
18
|
export const AiConversationIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character AI conversation ID.");
|
|
17
19
|
|
|
18
20
|
export type AiChatTaskView = {
|
|
21
|
+
nextRunAt?: string | null;
|
|
19
22
|
id: string;
|
|
20
23
|
chatId: string;
|
|
21
24
|
chatTitle: string;
|
|
25
|
+
grants: CapabilityGrant[];
|
|
22
26
|
prompt: string;
|
|
23
27
|
schedule: AiChatTaskSchedule;
|
|
24
28
|
timezone: string;
|
|
@@ -35,6 +39,7 @@ export type AiChatTaskOccurrenceView = {
|
|
|
35
39
|
trigger: "scheduled" | "manual";
|
|
36
40
|
state: AiChatTaskOccurrenceState;
|
|
37
41
|
error: string | null;
|
|
42
|
+
resultText: string | null;
|
|
38
43
|
createdAt: string;
|
|
39
44
|
startedAt: string | null;
|
|
40
45
|
completedAt: string | null;
|
|
@@ -44,6 +49,7 @@ export const toAiChatTaskView = (task: AiChatTask): AiChatTaskView => ({
|
|
|
44
49
|
id: task.shortId,
|
|
45
50
|
chatId: task.chatId,
|
|
46
51
|
chatTitle: task.chatTitle,
|
|
52
|
+
grants: task.grants,
|
|
47
53
|
prompt: task.prompt,
|
|
48
54
|
schedule: task.schedule,
|
|
49
55
|
timezone: task.timezone,
|
|
@@ -60,6 +66,7 @@ export const toAiChatTaskOccurrenceView = (occurrence: AiChatTaskOccurrence, tas
|
|
|
60
66
|
trigger: occurrence.trigger,
|
|
61
67
|
state: occurrence.state,
|
|
62
68
|
error: occurrence.error,
|
|
69
|
+
resultText: occurrence.resultText,
|
|
63
70
|
createdAt: occurrence.createdAt,
|
|
64
71
|
startedAt: occurrence.startedAt,
|
|
65
72
|
completedAt: occurrence.completedAt,
|
|
@@ -92,9 +99,10 @@ export const chatTaskCreateFingerprint = (input: {
|
|
|
92
99
|
prompt: string;
|
|
93
100
|
schedule: z.infer<typeof ChatTaskScheduleInputSchema>;
|
|
94
101
|
timezone?: string;
|
|
102
|
+
grants?: CapabilityGrant[];
|
|
95
103
|
}): string =>
|
|
96
104
|
new Bun.CryptoHasher("sha256")
|
|
97
|
-
.update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone ?? null]))
|
|
105
|
+
.update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone ?? null, input.grants ?? []]))
|
|
98
106
|
.digest("hex");
|
|
99
107
|
|
|
100
108
|
export const normalizeChatTaskSchedule = async (
|