@k2b/cloud 0.26.0 → 0.28.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 +3 -3
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/browser-code-contracts.ts +46 -64
- package/src/ai/browser.ts +9 -1
- package/src/ai/capabilities.ts +82 -22
- package/src/ai/chat/blocks.tsx +16 -7
- package/src/ai/chat/builtin-tools.tsx +31 -23
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -5
- package/src/ai/chat/message-actions.tsx +123 -101
- package/src/ai/chat/message-utils.ts +30 -1
- package/src/ai/chat/messages.ts +206 -2
- package/src/ai/chat/presentation.tsx +94 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-view.tsx +102 -16
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/client/controller.ts +83 -37
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +7 -2
- package/src/ai/code-mode-skill.ts +32 -36
- package/src/ai/code-runtime-tools.ts +14 -1
- package/src/ai/code-source-contracts.ts +56 -6
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +15 -16
- package/src/ai/executor.ts +281 -141
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +9 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/protocol.ts +6 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +2 -2
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +40 -6
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +9 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +35 -7
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +155 -71
- package/src/ai/stream.ts +180 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +8 -0
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-failure.ts +100 -0
- package/src/ai/turn-policy.ts +248 -0
- package/src/ai/types.ts +52 -4
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +242 -0
- package/src/api/index.ts +2 -0
- package/src/browser/FileChooser.tsx +11 -0
- package/src/browser/file-chooser-messages.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +8 -0
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +215 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +265 -0
- package/src/contracts/registry.ts +2 -0
- package/src/services/help/store.ts +2 -1
- package/src/services/index.ts +13 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +57 -5
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -45
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +112 -0
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +397 -0
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +17 -0
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/core-settings.ts +19 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +49 -5
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +3 -1
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +15 -17
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
|
@@ -145,6 +145,12 @@ function ListAppsView(props: { block: ToolBlock }) {
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Entries are `{name}` records; turns stored before #528 hold plain names and a `missing` list. */
|
|
149
|
+
const toolNames = (value: unknown): string[] =>
|
|
150
|
+
Array.isArray(value)
|
|
151
|
+
? value.flatMap((item) => (typeof item === "string" ? [item] : isRecord(item) && typeof item.name === "string" ? [item.name] : []))
|
|
152
|
+
: [];
|
|
153
|
+
|
|
148
154
|
function LoadToolsView(props: { block: ToolBlock }) {
|
|
149
155
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
150
156
|
const titles = (): Record<string, unknown> => {
|
|
@@ -152,31 +158,33 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
152
158
|
return isRecord(value) ? value : {};
|
|
153
159
|
};
|
|
154
160
|
const t = useMessages();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
const unavailableLabel = (reason: unknown) =>
|
|
162
|
+
reason === "not_offered_in_turn"
|
|
163
|
+
? t().toolNotOffered
|
|
164
|
+
: reason === "not_allowed"
|
|
165
|
+
? t().toolNotAllowed
|
|
166
|
+
: reason === "app_offline"
|
|
167
|
+
? t().toolAppOffline
|
|
168
|
+
: t().toolUnknown;
|
|
169
|
+
const rows = () => [
|
|
170
|
+
...toolNames(result().loaded).map((name) => ({ name, label: t().toolsLoaded })),
|
|
171
|
+
...toolNames(result().alreadyLoaded).map((name) => ({ name, label: t().toolsAlreadyLoaded })),
|
|
172
|
+
...records(result().unavailable).flatMap((item) =>
|
|
173
|
+
typeof item.name === "string" ? [{ name: item.name, label: unavailableLabel(item.reason) }] : [],
|
|
174
|
+
),
|
|
175
|
+
...toolNames(result().missing).map((name) => ({ name, label: t().toolUnknown })),
|
|
176
|
+
...toolNames(result().evicted).map((name) => ({ name, label: t().toolsEvicted })),
|
|
177
|
+
];
|
|
170
178
|
return (
|
|
171
|
-
<CompletedActivity
|
|
172
|
-
|
|
179
|
+
<CompletedActivity
|
|
180
|
+
block={props.block}
|
|
181
|
+
label={t().loadTools}
|
|
182
|
+
description={t().loadedCount({ count: toolNames(result().loaded).length })}
|
|
183
|
+
>
|
|
184
|
+
<Show when={rows().length > 0}>
|
|
173
185
|
<ResultList>
|
|
174
|
-
<For each={
|
|
175
|
-
{(
|
|
176
|
-
<For each={group.names}>
|
|
177
|
-
{(name) => <ResultRow icon={aiToolIcon(name)} title={text(titles()[name]) || name} meta={group.label} />}
|
|
178
|
-
</For>
|
|
179
|
-
)}
|
|
186
|
+
<For each={rows()}>
|
|
187
|
+
{(row) => <ResultRow icon={aiToolIcon(row.name)} title={text(titles()[row.name]) || row.name} meta={row.label} />}
|
|
180
188
|
</For>
|
|
181
189
|
</ResultList>
|
|
182
190
|
</Show>
|
|
@@ -40,7 +40,10 @@ export function PresentToolBlock(props: { block: ToolBlock }) {
|
|
|
40
40
|
</Show>
|
|
41
41
|
}
|
|
42
42
|
>
|
|
43
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
data-call-id={props.block.callId}
|
|
45
|
+
class="inline-flex min-h-7 max-w-full items-center gap-1.5 py-1 text-xs leading-none text-dimmed"
|
|
46
|
+
>
|
|
44
47
|
<i class={`ti ${icon()} shrink-0 text-base leading-none`} aria-hidden="true" />
|
|
45
48
|
<Show
|
|
46
49
|
when={actions.onOpenFile}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Chat } from "@k2b/ui";
|
|
2
|
-
import { createMemo } from "solid-js";
|
|
2
|
+
import { createMemo, createSignal } from "solid-js";
|
|
3
3
|
import { createStore, reconcile } from "solid-js/store";
|
|
4
4
|
import { render } from "solid-js/web";
|
|
5
5
|
import { emptyProjection, reduceProjection, visibleMessages } from "../client/projection";
|
|
@@ -8,11 +8,17 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The live chat timeline for a browser test, folded from wire events like the
|
|
11
|
-
* controller does. `window.emit(event)` applies one event,
|
|
12
|
-
* `window.steer(text)` appends a pending steer as the controller does
|
|
11
|
+
* controller does. `window.emit(event)` applies one event,
|
|
12
|
+
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
|
+
* `window.reconnect(on)` sets whether the stream reconnects, and
|
|
14
|
+
* `window.continued` lists the messages that **Continue** sent.
|
|
13
15
|
*/
|
|
14
16
|
const [state, setState] = createStore(emptyProjection());
|
|
17
|
+
const [reconnecting, setReconnecting] = createSignal(false);
|
|
18
|
+
const continued: string[] = [];
|
|
15
19
|
Object.assign(window, {
|
|
20
|
+
continued,
|
|
21
|
+
reconnect: (on: boolean) => setReconnecting(on),
|
|
16
22
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
17
23
|
steer: (text: string) =>
|
|
18
24
|
setState("activeTurn", "blocks", (blocks): AiTurnBlock[] => [
|
|
@@ -26,9 +32,13 @@ root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max
|
|
|
26
32
|
document.body.append(root);
|
|
27
33
|
render(
|
|
28
34
|
() => (
|
|
29
|
-
<AiChatActionsProvider actions={{}}>
|
|
35
|
+
<AiChatActionsProvider actions={{ onContinueTurn: (message) => void continued.push(message) }}>
|
|
30
36
|
{(() => {
|
|
31
|
-
const items = createAiChatTimeline({
|
|
37
|
+
const items = createAiChatTimeline({
|
|
38
|
+
messages: createMemo(() => visibleMessages(state)),
|
|
39
|
+
activeTurn: () => state.activeTurn,
|
|
40
|
+
reconnecting,
|
|
41
|
+
});
|
|
32
42
|
return <Chat.Timeline items={items()} />;
|
|
33
43
|
})()}
|
|
34
44
|
</AiChatActionsProvider>
|
|
@@ -7,15 +7,20 @@ import {
|
|
|
7
7
|
PanelDialog,
|
|
8
8
|
panelDialogFixedOptions,
|
|
9
9
|
prompts,
|
|
10
|
-
StatCell,
|
|
11
|
-
StatGrid,
|
|
12
10
|
TextInput,
|
|
13
11
|
useLocale,
|
|
14
12
|
} from "@k2b/ui";
|
|
15
13
|
import { createContext, createSignal, For, type JSX, Show, useContext } from "solid-js";
|
|
16
14
|
import type { AiTurnBlock } from "../protocol";
|
|
17
15
|
import type { AiMessageFeedback, AiMessageFeedbackReason, AiStoredMessage } from "../types";
|
|
18
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AiForkMessageInput,
|
|
18
|
+
type AiRetryMessageInput,
|
|
19
|
+
aiToolIcon,
|
|
20
|
+
describeResponseEnd,
|
|
21
|
+
displayToolName,
|
|
22
|
+
formatWorkedDuration,
|
|
23
|
+
} from "./message-utils";
|
|
19
24
|
import { aiChatMessages } from "./messages";
|
|
20
25
|
|
|
21
26
|
/** The active-turn coordinates an approval/tool action needs to resolve on the server. */
|
|
@@ -41,6 +46,11 @@ export type AiChatActions = {
|
|
|
41
46
|
onForkMessage?: ForkMessageHandler;
|
|
42
47
|
onRetryMessage?: RetryMessageHandler;
|
|
43
48
|
onRetrySteer?: RetrySteerHandler;
|
|
49
|
+
/**
|
|
50
|
+
* Continue after a failed turn. Send `message`, a visible request in the reader's language to pick up where the
|
|
51
|
+
* turn stopped, through the host's normal send path. Without it, a failed turn shows its reason only.
|
|
52
|
+
*/
|
|
53
|
+
onContinueTurn?: (message: string) => void | Promise<void>;
|
|
44
54
|
onMessageFeedback?: (entry: AiStoredMessage, feedback: Omit<AiMessageFeedback, "updatedAt"> | null) => void | Promise<void>;
|
|
45
55
|
/** Open the originating background run for a delivered scheduled result. */
|
|
46
56
|
onOpenScheduledTaskRun?: (taskId: string, occurrenceId: string) => void;
|
|
@@ -72,6 +82,7 @@ const formatDateTime = (value: string, locale: string) =>
|
|
|
72
82
|
new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
|
73
83
|
|
|
74
84
|
const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
85
|
+
const t = aiChatMessages(locale);
|
|
75
86
|
const assistantEntries = entries.filter((entry) => entry.kind === "message" && entry.message.role === "assistant");
|
|
76
87
|
const entry = assistantEntries.findLast((candidate) => candidate.loopAggregate) ?? assistantEntries.at(-1) ?? entries.at(-1);
|
|
77
88
|
if (!entry) return null;
|
|
@@ -83,18 +94,18 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
83
94
|
const timing = aggregate?.timing;
|
|
84
95
|
|
|
85
96
|
const meta = [
|
|
86
|
-
{ label:
|
|
87
|
-
{ label:
|
|
88
|
-
{ label:
|
|
89
|
-
{ label:
|
|
90
|
-
{ label:
|
|
97
|
+
{ label: t.infoProviderModel, value: entry.providerModel ?? t.infoUnknown },
|
|
98
|
+
{ label: t.infoModelProfile, value: entry.modelProfileId ?? t.infoUnknown },
|
|
99
|
+
{ label: t.infoLoopId, value: entry.loopId ?? t.infoLegacy },
|
|
100
|
+
{ label: t.infoFinished, value: describeResponseEnd(entry, locale) },
|
|
101
|
+
{ label: t.infoCreated, value: formatDateTime(entry.createdAt, locale) },
|
|
91
102
|
];
|
|
92
103
|
|
|
93
104
|
const issues = [
|
|
94
|
-
{ label:
|
|
95
|
-
{ label:
|
|
96
|
-
{ label:
|
|
97
|
-
{ label:
|
|
105
|
+
{ label: t.infoIssueErrors, count: aggregate?.toolErrorCount ?? 0 },
|
|
106
|
+
{ label: t.infoIssueStream, count: aggregate?.toolIssueCount ?? 0 },
|
|
107
|
+
{ label: t.infoIssueMalformed, count: aggregate?.toolMalformedCount ?? 0 },
|
|
108
|
+
{ label: t.infoIssueCancelled, count: aggregate?.toolCancelledCount ?? 0 },
|
|
98
109
|
].filter((issue) => issue.count > 0);
|
|
99
110
|
|
|
100
111
|
const toolCounts = new Map<string, number>();
|
|
@@ -117,6 +128,27 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
117
128
|
};
|
|
118
129
|
};
|
|
119
130
|
|
|
131
|
+
/** Label and value pairs in one quiet list; a hint explains a value in muted text below it. */
|
|
132
|
+
function InfoRows(props: { rows: readonly { label: string; value: string; hint?: string }[] }) {
|
|
133
|
+
return (
|
|
134
|
+
<dl class="ai-response-info__rows">
|
|
135
|
+
<For each={props.rows}>
|
|
136
|
+
{(row) => (
|
|
137
|
+
<div>
|
|
138
|
+
<dt>
|
|
139
|
+
{row.label}
|
|
140
|
+
<Show when={row.hint}>
|
|
141
|
+
<span>{row.hint}</span>
|
|
142
|
+
</Show>
|
|
143
|
+
</dt>
|
|
144
|
+
<dd title={row.value}>{row.value}</dd>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</For>
|
|
148
|
+
</dl>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
121
153
|
const info = assistantResponseInfo(entries, locale);
|
|
122
154
|
if (!info) return;
|
|
@@ -130,85 +162,70 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
130
162
|
void dialogCore.open<void>(
|
|
131
163
|
(close) => (
|
|
132
164
|
<PanelDialog>
|
|
133
|
-
<PanelDialog.Header title=
|
|
165
|
+
<PanelDialog.Header title={t.actionMessageInfo} subtitle={t.infoSubtitle} icon="ti ti-info-circle" close={close} />
|
|
134
166
|
<PanelDialog.Body>
|
|
135
|
-
<PanelDialog.Section title=
|
|
136
|
-
<
|
|
137
|
-
<For each={info.meta}>
|
|
138
|
-
{(row) => (
|
|
139
|
-
<>
|
|
140
|
-
<dt class="text-dimmed">{row.label}</dt>
|
|
141
|
-
<dd class="min-w-0 truncate text-right text-primary" title={row.value}>
|
|
142
|
-
{row.value}
|
|
143
|
-
</dd>
|
|
144
|
-
</>
|
|
145
|
-
)}
|
|
146
|
-
</For>
|
|
147
|
-
</dl>
|
|
167
|
+
<PanelDialog.Section title={t.infoDetails} icon="ti ti-list-details">
|
|
168
|
+
<InfoRows rows={info.meta} />
|
|
148
169
|
</PanelDialog.Section>
|
|
149
170
|
|
|
150
171
|
<Show when={info.timing}>
|
|
151
172
|
{(timing) => (
|
|
152
|
-
<
|
|
153
|
-
{/* Worked =
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
<PanelDialog.Section title={t.infoTiming} icon="ti ti-clock">
|
|
174
|
+
{/* Worked = writing + tool execution; waiting for approvals and answers is listed separately. */}
|
|
175
|
+
<InfoRows
|
|
176
|
+
rows={[
|
|
177
|
+
{ label: t.infoWorked, value: formatWorkedDuration(timing().totalElapsedMs), hint: t.infoWorkedHint },
|
|
178
|
+
{ label: t.infoGeneration, value: formatWorkedDuration(timing().generationMs) },
|
|
179
|
+
{ label: t.infoToolTime, value: timing().toolExecutionMs > 0 ? formatWorkedDuration(timing().toolExecutionMs) : "–" },
|
|
180
|
+
{
|
|
181
|
+
label: t.infoWaiting,
|
|
182
|
+
value: timing().actionWaitMs > 0 ? formatWorkedDuration(timing().actionWaitMs) : "–",
|
|
183
|
+
hint: t.infoWaitingHint,
|
|
184
|
+
},
|
|
185
|
+
{ label: t.infoWallClock, value: formatWorkedDuration(timing().wallMs) },
|
|
186
|
+
{
|
|
187
|
+
label: t.infoSpeed,
|
|
188
|
+
value:
|
|
189
|
+
timing().outputTokensPerSecond !== undefined
|
|
190
|
+
? t.infoTokensPerSecond({
|
|
191
|
+
value: timing().outputTokensPerSecond!.toLocaleString(locale, { maximumFractionDigits: 1 }),
|
|
192
|
+
})
|
|
193
|
+
: "–",
|
|
194
|
+
},
|
|
195
|
+
]}
|
|
173
196
|
/>
|
|
174
|
-
</
|
|
197
|
+
</PanelDialog.Section>
|
|
175
198
|
)}
|
|
176
199
|
</Show>
|
|
177
200
|
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
value={number(issueTotal)}
|
|
191
|
-
sub={issueSummary}
|
|
192
|
-
accent={{ tone: "red", icon: "ti ti-alert-triangle" }}
|
|
193
|
-
/>
|
|
194
|
-
</Show>
|
|
195
|
-
</StatGrid>
|
|
201
|
+
<PanelDialog.Section title={t.infoUsage} icon="ti ti-chart-bar">
|
|
202
|
+
<InfoRows
|
|
203
|
+
rows={[
|
|
204
|
+
{ label: t.infoInputTokens, value: tokens(info.usage.input) },
|
|
205
|
+
{ label: t.infoOutputTokens, value: tokens(info.usage.output) },
|
|
206
|
+
{ label: t.infoTotalTokens, value: tokens(info.usage.total) },
|
|
207
|
+
{ label: t.assistantTurns, value: number(info.turns) },
|
|
208
|
+
{ label: t.toolCalls, value: number(info.toolCallCount) },
|
|
209
|
+
{ label: t.toolIssues, value: info.issues.length > 0 ? `${number(issueTotal)} · ${issueSummary}` : t.none },
|
|
210
|
+
]}
|
|
211
|
+
/>
|
|
212
|
+
</PanelDialog.Section>
|
|
196
213
|
|
|
197
214
|
<Show when={info.tools.length > 0}>
|
|
198
|
-
<PanelDialog.Section title=
|
|
199
|
-
<
|
|
215
|
+
<PanelDialog.Section title={t.infoTools} icon="ti ti-tool" subtitle={t.infoToolsHint}>
|
|
216
|
+
<ul class="ai-response-info__tools">
|
|
200
217
|
<For each={info.tools}>
|
|
201
218
|
{(tool) => (
|
|
202
|
-
<
|
|
203
|
-
<i class={
|
|
219
|
+
<li>
|
|
220
|
+
<i class={aiToolIcon(tool.name)} aria-hidden="true" />
|
|
204
221
|
{displayToolName(tool.name)}
|
|
205
222
|
<Show when={tool.count > 1}>
|
|
206
|
-
<span class="
|
|
223
|
+
<span class="text-dimmed tabular-nums">×{tool.count}</span>
|
|
207
224
|
</Show>
|
|
208
|
-
</
|
|
225
|
+
</li>
|
|
209
226
|
)}
|
|
210
227
|
</For>
|
|
211
|
-
</
|
|
228
|
+
</ul>
|
|
212
229
|
</PanelDialog.Section>
|
|
213
230
|
</Show>
|
|
214
231
|
</PanelDialog.Body>
|
|
@@ -218,12 +235,12 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
218
235
|
);
|
|
219
236
|
};
|
|
220
237
|
|
|
221
|
-
const forkTitleFromText = (text: string): string => {
|
|
238
|
+
const forkTitleFromText = (text: string, fallback: string): string => {
|
|
222
239
|
const firstLine = text
|
|
223
240
|
.split("\n")
|
|
224
241
|
.map((line) => line.trim())
|
|
225
242
|
.find(Boolean);
|
|
226
|
-
if (!firstLine) return
|
|
243
|
+
if (!firstLine) return fallback;
|
|
227
244
|
return firstLine.length > 80 ? `${firstLine.slice(0, 77).trim()}...` : firstLine;
|
|
228
245
|
};
|
|
229
246
|
|
|
@@ -231,17 +248,19 @@ const openForkMessageDialog = async (
|
|
|
231
248
|
entry: AiStoredMessage,
|
|
232
249
|
copyText: string,
|
|
233
250
|
onForkMessage: (entry: AiStoredMessage, input?: AiForkMessageInput) => void | Promise<void>,
|
|
251
|
+
locale: string,
|
|
234
252
|
) => {
|
|
253
|
+
const t = aiChatMessages(locale);
|
|
235
254
|
const result = await prompts.form({
|
|
236
|
-
title:
|
|
255
|
+
title: t.forkTitle,
|
|
237
256
|
icon: "ti ti-git-fork",
|
|
238
|
-
confirmText:
|
|
257
|
+
confirmText: t.forkConfirm,
|
|
239
258
|
size: "medium",
|
|
240
259
|
fields: {
|
|
241
260
|
title: {
|
|
242
261
|
type: "text",
|
|
243
|
-
label:
|
|
244
|
-
default: forkTitleFromText(copyText),
|
|
262
|
+
label: t.forkName,
|
|
263
|
+
default: forkTitleFromText(copyText, t.forkDefaultName),
|
|
245
264
|
required: true,
|
|
246
265
|
maxLength: 120,
|
|
247
266
|
},
|
|
@@ -251,16 +270,18 @@ const openForkMessageDialog = async (
|
|
|
251
270
|
if (title) await onForkMessage(entry, { title });
|
|
252
271
|
};
|
|
253
272
|
|
|
254
|
-
const feedbackReasons
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{ id: "
|
|
258
|
-
{ id: "
|
|
259
|
-
{ id: "
|
|
260
|
-
{ id: "
|
|
273
|
+
const feedbackReasons = (
|
|
274
|
+
t: ReturnType<typeof aiChatMessages>,
|
|
275
|
+
): readonly { id: AiMessageFeedbackReason; label: string; description: string }[] => [
|
|
276
|
+
{ id: "incorrect", label: t.feedbackIncorrect, description: t.feedbackIncorrectHint },
|
|
277
|
+
{ id: "did_not_follow_request", label: t.feedbackIgnoredRequest, description: t.feedbackIgnoredRequestHint },
|
|
278
|
+
{ id: "incomplete", label: t.feedbackIncomplete, description: t.feedbackIncompleteHint },
|
|
279
|
+
{ id: "poor_tool_choice", label: t.feedbackWrongApp, description: t.feedbackWrongAppHint },
|
|
280
|
+
{ id: "too_slow", label: t.feedbackSlow, description: t.feedbackSlowHint },
|
|
281
|
+
{ id: "other", label: t.feedbackOther, description: t.feedbackOtherHint },
|
|
261
282
|
];
|
|
262
283
|
|
|
263
|
-
const openNegativeFeedbackDialog = () =>
|
|
284
|
+
const openNegativeFeedbackDialog = (t: ReturnType<typeof aiChatMessages>) =>
|
|
264
285
|
prompts.dialog<{ reasons: AiMessageFeedbackReason[]; comment: string | null } | undefined>(
|
|
265
286
|
(close) => {
|
|
266
287
|
const [selected, setSelected] = createSignal<AiMessageFeedbackReason[]>([]);
|
|
@@ -270,9 +291,9 @@ const openNegativeFeedbackDialog = () =>
|
|
|
270
291
|
const valid = () => selected().length > 0 || comment().trim().length > 0;
|
|
271
292
|
return (
|
|
272
293
|
<div class="flex min-h-0 flex-col gap-4">
|
|
273
|
-
<p class="text-sm text-secondary">
|
|
294
|
+
<p class="text-sm text-secondary">{t.feedbackIntro}</p>
|
|
274
295
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
275
|
-
<For each={feedbackReasons}>
|
|
296
|
+
<For each={feedbackReasons(t)}>
|
|
276
297
|
{(reason) => (
|
|
277
298
|
<CheckboxCard
|
|
278
299
|
label={reason.label}
|
|
@@ -284,8 +305,8 @@ const openNegativeFeedbackDialog = () =>
|
|
|
284
305
|
</For>
|
|
285
306
|
</div>
|
|
286
307
|
<TextInput
|
|
287
|
-
label=
|
|
288
|
-
description=
|
|
308
|
+
label={t.feedbackDetails}
|
|
309
|
+
description={t.feedbackDetailsHint}
|
|
289
310
|
multiline
|
|
290
311
|
lines={3}
|
|
291
312
|
maxLength={1_000}
|
|
@@ -294,7 +315,7 @@ const openNegativeFeedbackDialog = () =>
|
|
|
294
315
|
/>
|
|
295
316
|
<div class="flex justify-end gap-2">
|
|
296
317
|
<Button variant="secondary" size="sm" type="button" onClick={() => close(undefined)}>
|
|
297
|
-
|
|
318
|
+
{t.cancel}
|
|
298
319
|
</Button>
|
|
299
320
|
<Button
|
|
300
321
|
size="sm"
|
|
@@ -302,13 +323,13 @@ const openNegativeFeedbackDialog = () =>
|
|
|
302
323
|
disabled={!valid()}
|
|
303
324
|
onClick={() => close({ reasons: selected(), comment: comment().trim() || null })}
|
|
304
325
|
>
|
|
305
|
-
|
|
326
|
+
{t.feedbackSend}
|
|
306
327
|
</Button>
|
|
307
328
|
</div>
|
|
308
329
|
</div>
|
|
309
330
|
);
|
|
310
331
|
},
|
|
311
|
-
{ title:
|
|
332
|
+
{ title: t.feedbackTitle, icon: "ti ti-message-report", size: "large" },
|
|
312
333
|
);
|
|
313
334
|
|
|
314
335
|
export function createAssistantMessageActions(props: {
|
|
@@ -318,13 +339,14 @@ export function createAssistantMessageActions(props: {
|
|
|
318
339
|
actions: AiChatActions;
|
|
319
340
|
}): ChatAction[] {
|
|
320
341
|
const locale = useLocale();
|
|
342
|
+
const t = aiChatMessages(locale());
|
|
321
343
|
const actions = props.actions;
|
|
322
344
|
const result: ChatAction[] = [
|
|
323
345
|
...(actions.onMessageFeedback
|
|
324
346
|
? [
|
|
325
347
|
{
|
|
326
348
|
id: "feedback-up",
|
|
327
|
-
label: props.entry.feedback?.rating === "up" ?
|
|
349
|
+
label: props.entry.feedback?.rating === "up" ? t.actionHelpfulRemove : t.actionHelpful,
|
|
328
350
|
icon: "ti ti-thumb-up",
|
|
329
351
|
pressed: props.entry.feedback?.rating === "up",
|
|
330
352
|
pressedTone: "success" as const,
|
|
@@ -336,13 +358,13 @@ export function createAssistantMessageActions(props: {
|
|
|
336
358
|
},
|
|
337
359
|
{
|
|
338
360
|
id: "feedback-down",
|
|
339
|
-
label: props.entry.feedback?.rating === "down" ?
|
|
361
|
+
label: props.entry.feedback?.rating === "down" ? t.actionImproveRemove : t.actionImprove,
|
|
340
362
|
icon: "ti ti-thumb-down",
|
|
341
363
|
pressed: props.entry.feedback?.rating === "down",
|
|
342
364
|
pressedTone: "danger" as const,
|
|
343
365
|
onSelect: async () => {
|
|
344
366
|
if (props.entry.feedback?.rating === "down") return actions.onMessageFeedback!(props.entry, null);
|
|
345
|
-
const feedback = await openNegativeFeedbackDialog();
|
|
367
|
+
const feedback = await openNegativeFeedbackDialog(t);
|
|
346
368
|
if (feedback) await actions.onMessageFeedback!(props.entry, { rating: "down", ...feedback });
|
|
347
369
|
},
|
|
348
370
|
},
|
|
@@ -350,7 +372,7 @@ export function createAssistantMessageActions(props: {
|
|
|
350
372
|
: []),
|
|
351
373
|
{
|
|
352
374
|
id: "info",
|
|
353
|
-
label:
|
|
375
|
+
label: t.actionMessageInfo,
|
|
354
376
|
icon: "ti ti-info-circle",
|
|
355
377
|
onSelect: () => openAssistantResponseInfo(props.entries, locale()),
|
|
356
378
|
},
|
|
@@ -358,7 +380,7 @@ export function createAssistantMessageActions(props: {
|
|
|
358
380
|
if (props.copyText) {
|
|
359
381
|
result.push({
|
|
360
382
|
id: "copy",
|
|
361
|
-
label:
|
|
383
|
+
label: t.actionCopy,
|
|
362
384
|
icon: "ti ti-copy",
|
|
363
385
|
copyText: props.copyText,
|
|
364
386
|
});
|
|
@@ -366,9 +388,9 @@ export function createAssistantMessageActions(props: {
|
|
|
366
388
|
if (actions.onForkMessage) {
|
|
367
389
|
result.push({
|
|
368
390
|
id: "fork",
|
|
369
|
-
label:
|
|
391
|
+
label: t.actionFork,
|
|
370
392
|
icon: "ti ti-git-fork",
|
|
371
|
-
onSelect: () => openForkMessageDialog(props.entry, props.copyText, actions.onForkMessage
|
|
393
|
+
onSelect: () => openForkMessageDialog(props.entry, props.copyText, actions.onForkMessage!, locale()),
|
|
372
394
|
});
|
|
373
395
|
}
|
|
374
396
|
return result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, Usage } from "@k2b/nessi";
|
|
1
|
+
import type { DoneReason, Message, Usage } from "@k2b/nessi";
|
|
2
2
|
import { fileIcons } from "@k2b/stdlib";
|
|
3
3
|
import { formatBytes as sharedFormatBytes } from "../../shared/format";
|
|
4
4
|
import { type AiAttachmentRef, parseAiAttachmentMarkers } from "../attachments";
|
|
@@ -122,6 +122,35 @@ export const FILE_INPUT_ACCEPT = [
|
|
|
122
122
|
].join(",");
|
|
123
123
|
|
|
124
124
|
/** Seconds-granular work duration ("8s", "2m 14s") — stdlib's dates.formatDuration is deliberately minute-granular. */
|
|
125
|
+
/**
|
|
126
|
+
* How a response ended, in words: the run's end, plus the last message's own stop when it adds something, such as a
|
|
127
|
+
* cut-off answer. A stop code the list does not know stays as the provider sent it.
|
|
128
|
+
*/
|
|
129
|
+
export const describeResponseEnd = (entry: Pick<AiStoredMessage, "loopDoneReason" | "stopReason">, locale: string): string => {
|
|
130
|
+
const t = aiChatMessages(locale);
|
|
131
|
+
const run: Record<DoneReason, string> = {
|
|
132
|
+
stop: t.infoEndCompleted,
|
|
133
|
+
aborted: t.infoEndStopped,
|
|
134
|
+
error: t.infoEndFailed,
|
|
135
|
+
max_turns: t.infoEndStepLimit,
|
|
136
|
+
no_credits: t.infoEndUsageLimit,
|
|
137
|
+
context_overflow: t.infoEndContextFull,
|
|
138
|
+
};
|
|
139
|
+
const message: Record<string, string | null> = {
|
|
140
|
+
stop: entry.loopDoneReason ? null : t.infoEndCompleted,
|
|
141
|
+
tool_use: null,
|
|
142
|
+
max_tokens: t.infoEndLengthLimit,
|
|
143
|
+
aborted: t.infoEndStopped,
|
|
144
|
+
interrupted: t.infoEndInterrupted,
|
|
145
|
+
error: t.infoEndFailed,
|
|
146
|
+
};
|
|
147
|
+
const parts = [
|
|
148
|
+
entry.loopDoneReason ? run[entry.loopDoneReason] : null,
|
|
149
|
+
entry.stopReason ? (Object.hasOwn(message, entry.stopReason) ? message[entry.stopReason] : entry.stopReason) : null,
|
|
150
|
+
].filter((part): part is string => Boolean(part));
|
|
151
|
+
return [...new Set(parts)].join(" · ") || t.infoUnknown;
|
|
152
|
+
};
|
|
153
|
+
|
|
125
154
|
export const formatWorkedDuration = (ms: number): string => {
|
|
126
155
|
const totalSeconds = Math.max(1, Math.round(ms / 1000));
|
|
127
156
|
const hours = Math.floor(totalSeconds / 3600);
|