@k2b/cloud 0.30.0 → 0.32.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 -4
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/capabilities.ts +2 -2
- package/src/_internal/define-app.ts +17 -5
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +64 -39
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +9 -1
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +34 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +67 -12
- package/src/ai/chat/tool-names.ts +18 -2
- package/src/ai/chat/turn-layout.ts +8 -0
- package/src/ai/chat/turn-view.tsx +111 -32
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/code-runtime-tools.ts +50 -17
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +343 -45
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +98 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +120 -2
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +28 -3
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +99 -49
- package/src/ai/runtime.ts +90 -15
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +91 -1
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tools.ts +94 -3
- package/src/ai/types.ts +48 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/capabilities.ts +3 -2
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/api/widgets.ts +36 -5
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +7 -5
- package/src/capabilities/executions.ts +3 -0
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +24 -2
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/app.ts +34 -2
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/registry.ts +9 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +47 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/server/widget-request.ts +27 -0
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +241 -0
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +41 -0
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +46 -7
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +265 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +39 -3
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CopyButton,
|
|
3
|
+
DataTable,
|
|
4
|
+
type DataTableSort,
|
|
5
|
+
DescriptionList,
|
|
6
|
+
StatCell,
|
|
7
|
+
type StatCellAccent,
|
|
8
|
+
StatGrid,
|
|
9
|
+
type StatGridColumns,
|
|
10
|
+
useLocale,
|
|
11
|
+
} from "@k2b/ui";
|
|
12
|
+
import { createMemo, createSignal, createUniqueId, Index, type JSX, Match, onCleanup, onMount, Show, Switch } from "solid-js";
|
|
13
|
+
import type { CloudAiChartRow } from "../chart-block";
|
|
14
|
+
import {
|
|
15
|
+
CLOUD_AI_DETAILS_VIEWPORT_ITEMS,
|
|
16
|
+
CLOUD_AI_VIEW_MAX_BYTES,
|
|
17
|
+
type CloudAiDetailsView,
|
|
18
|
+
type CloudAiExplorerView,
|
|
19
|
+
type CloudAiStatsView,
|
|
20
|
+
type CloudAiTableView,
|
|
21
|
+
type CloudAiView,
|
|
22
|
+
type CloudAiViewSummary,
|
|
23
|
+
cloudAiDetailsRows,
|
|
24
|
+
cloudAiStatsTiles,
|
|
25
|
+
cloudAiTableTsv,
|
|
26
|
+
cloudAiViewSummary,
|
|
27
|
+
cloudAiViewTable,
|
|
28
|
+
isAiViewFile,
|
|
29
|
+
parseCloudAiView,
|
|
30
|
+
sortCloudAiViewRows,
|
|
31
|
+
} from "../view-block";
|
|
32
|
+
import { CloudChartGroupView, CloudChartView } from "./chart-block";
|
|
33
|
+
import { CloudExplorerView } from "./explorer-block";
|
|
34
|
+
import { useAiChatActions } from "./message-actions";
|
|
35
|
+
import { aiChatMessages } from "./messages";
|
|
36
|
+
import { ReferenceText } from "./reference-text";
|
|
37
|
+
|
|
38
|
+
/** Rows a table shows inline, as capability tables do; sorting and Copy data use all of them. */
|
|
39
|
+
const VISIBLE_ROWS = 100;
|
|
40
|
+
/** Rows the viewport shows before it scrolls; it reserves their height from the first frame. */
|
|
41
|
+
const VIEWPORT_ROWS = 8;
|
|
42
|
+
/** A compact row: 12px text on a 16px line, 6px padding above and below, a 1px line. */
|
|
43
|
+
const ROW_REM = 1.8125;
|
|
44
|
+
|
|
45
|
+
/** Loads a view file with the viewer's access and validates it again: a file is untrusted whoever wrote it. */
|
|
46
|
+
async function loadView(url: string, path: string, signal: AbortSignal): Promise<CloudAiView | null> {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, { signal });
|
|
49
|
+
if (!response.ok || !isAiViewFile({ path, origin: response.headers.get("X-Cloud-Ai-File-Origin") ?? "" })) return null;
|
|
50
|
+
const blob = await response.blob();
|
|
51
|
+
if (blob.size > CLOUD_AI_VIEW_MAX_BYTES) return null;
|
|
52
|
+
return parseCloudAiView(JSON.parse(await blob.text())) ?? null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A view's title and subtitle: the loaded file's once it is there, its summary's before. */
|
|
59
|
+
function ViewHeading(props: { id: string; title: string; subtitle?: string; actions?: JSX.Element }) {
|
|
60
|
+
return (
|
|
61
|
+
<header class="flex flex-wrap items-center justify-between gap-2">
|
|
62
|
+
<div class="min-w-0">
|
|
63
|
+
<h3 id={props.id} class="m-0 text-base font-semibold">
|
|
64
|
+
{props.title}
|
|
65
|
+
</h3>
|
|
66
|
+
<Show when={props.subtitle}>{(subtitle) => <span class="text-sm text-secondary">{subtitle()}</span>}</Show>
|
|
67
|
+
</div>
|
|
68
|
+
{props.actions}
|
|
69
|
+
</header>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type TableSummary = Extract<CloudAiViewSummary, { view: "table" }>;
|
|
74
|
+
type ChartSummary = Extract<CloudAiViewSummary, { view: "chart" }>;
|
|
75
|
+
type GroupSummary = Extract<CloudAiViewSummary, { view: "group" }>;
|
|
76
|
+
type ExplorerSummary = Extract<CloudAiViewSummary, { view: "explorer" }>;
|
|
77
|
+
|
|
78
|
+
/** The header row, the rows up to the viewport's, and the totals row when there is one. */
|
|
79
|
+
const tableHeight = (summary: TableSummary) =>
|
|
80
|
+
`${(Math.max(1, Math.min(summary.rows, VIEWPORT_ROWS)) + 1 + Number(summary.footer)) * ROW_REM}rem`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A table view: title, Copy data and a sortable table in a viewport whose height follows the row count of its
|
|
84
|
+
* `present` summary, so loading, the table and the unavailable state take the same room.
|
|
85
|
+
*/
|
|
86
|
+
function CloudTableView(props: { summary: TableSummary; input: CloudAiTableView | null | undefined }) {
|
|
87
|
+
const locale = useLocale();
|
|
88
|
+
const t = () => aiChatMessages(locale());
|
|
89
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
90
|
+
const table = createMemo(() => (props.input ? cloudAiViewTable(props.input, locale()) : null));
|
|
91
|
+
// The view's own order applies until the reader picks one.
|
|
92
|
+
const [picked, setSort] = createSignal<DataTableSort>();
|
|
93
|
+
const sort = (): DataTableSort | null => {
|
|
94
|
+
const own = props.input?.sort;
|
|
95
|
+
return picked() ?? (own ? { key: own.column, direction: own.direction } : null);
|
|
96
|
+
};
|
|
97
|
+
// cloudAiViewTable returns the view's own order; a picked one sorts again.
|
|
98
|
+
const rows = createMemo(() => {
|
|
99
|
+
const value = table();
|
|
100
|
+
const order = picked();
|
|
101
|
+
return !value ? [] : order ? sortCloudAiViewRows(value.rows, order, locale()) : value.rows;
|
|
102
|
+
});
|
|
103
|
+
const copyText = createMemo(() => {
|
|
104
|
+
const value = table();
|
|
105
|
+
return value ? cloudAiTableTsv({ ...value, rows: rows() }) : "";
|
|
106
|
+
});
|
|
107
|
+
const number = () => new Intl.NumberFormat(locale());
|
|
108
|
+
const refColumns = createMemo(() => new Set(props.input?.columns.flatMap((column) => (column.type === "ref" ? [column.id] : []))));
|
|
109
|
+
return (
|
|
110
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
111
|
+
<ViewHeading
|
|
112
|
+
id={id}
|
|
113
|
+
title={props.input?.title ?? props.summary.title}
|
|
114
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
115
|
+
actions={<CopyButton text={copyText()} label={t().viewCopyData} iconOnly size="sm" variant="ghost" disabled={!table()} />}
|
|
116
|
+
/>
|
|
117
|
+
<div class="min-w-0" style={{ height: tableHeight(props.summary) }}>
|
|
118
|
+
<Show
|
|
119
|
+
when={table()}
|
|
120
|
+
fallback={
|
|
121
|
+
<div role="status" class="flex h-full items-center justify-center text-center text-xs text-dimmed">
|
|
122
|
+
{props.input === undefined ? t().viewLoading : t().viewUnavailable}
|
|
123
|
+
</div>
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
{(value) => (
|
|
127
|
+
<DataTable<CloudAiChartRow>
|
|
128
|
+
rows={rows().slice(0, VISIBLE_ROWS)}
|
|
129
|
+
columns={value().columns.map((column) => ({
|
|
130
|
+
id: column.id,
|
|
131
|
+
header: column.label,
|
|
132
|
+
// A wrapped label would make the header taller than the reserved row; the table scrolls sideways instead.
|
|
133
|
+
headerClass: "whitespace-nowrap",
|
|
134
|
+
value: (row) => row.cells[column.id] ?? "",
|
|
135
|
+
align: column.numeric ? "right" : "left",
|
|
136
|
+
sortable: true,
|
|
137
|
+
}))}
|
|
138
|
+
renderCell={({ row, col, value: text, render }) => {
|
|
139
|
+
// Row keys are row indexes, so a reference cell finds its object in the loaded view.
|
|
140
|
+
const cells = props.input?.rows[Number(row.key)];
|
|
141
|
+
const cell = refColumns().has(col.id) && cells && Object.hasOwn(cells, col.id) ? cells[col.id] : undefined;
|
|
142
|
+
return cell && typeof cell === "object" ? <ReferenceText text={String(text)} reference={cell} /> : render(text);
|
|
143
|
+
}}
|
|
144
|
+
getRowId={(row) => row.key}
|
|
145
|
+
sort={sort()}
|
|
146
|
+
onSortChange={setSort}
|
|
147
|
+
footer={
|
|
148
|
+
value().footer
|
|
149
|
+
? {
|
|
150
|
+
values: value().footer?.cells,
|
|
151
|
+
renderCell: ({ col, value }) => {
|
|
152
|
+
const text = typeof value === "string" ? value : "";
|
|
153
|
+
const cells = props.input?.footer;
|
|
154
|
+
const cell = refColumns().has(col.id) && cells && Object.hasOwn(cells, col.id) ? cells[col.id] : undefined;
|
|
155
|
+
return cell && typeof cell === "object" ? <ReferenceText text={text} reference={cell} /> : text;
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: undefined
|
|
159
|
+
}
|
|
160
|
+
ariaLabelledBy={id}
|
|
161
|
+
density="compact"
|
|
162
|
+
surface="paper"
|
|
163
|
+
stickyHeader
|
|
164
|
+
class="h-full"
|
|
165
|
+
empty={<span>{t().tableEmpty}</span>}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
<Show when={props.summary.rows > VISIBLE_ROWS}>
|
|
171
|
+
<p class="text-sm text-secondary">
|
|
172
|
+
{t().viewRowsCapped({ shown: number().format(VISIBLE_ROWS), total: number().format(props.summary.rows) })}
|
|
173
|
+
</p>
|
|
174
|
+
</Show>
|
|
175
|
+
</section>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type StatsSummary = Extract<CloudAiViewSummary, { view: "stats" }>;
|
|
180
|
+
type DetailsSummary = Extract<CloudAiViewSummary, { view: "details" }>;
|
|
181
|
+
|
|
182
|
+
const DELTA_TONES = { better: "emerald", worse: "red", neutral: "zinc" } as const;
|
|
183
|
+
const DELTA_ICONS = { up: "ti ti-trending-up", down: "ti ti-trending-down", flat: "ti ti-minus" } as const;
|
|
184
|
+
/** One column per tile, up to the grid's six; its ladder narrows them on phones. */
|
|
185
|
+
const STATS_COLUMNS: readonly StatGridColumns[] = [1, 2, 3, 4, 5, 6];
|
|
186
|
+
|
|
187
|
+
/** The host picks the change's color from whether it is better; the model never chooses tones. */
|
|
188
|
+
const deltaAccent = (delta: ReturnType<typeof cloudAiStatsTiles>[number]["delta"]): StatCellAccent | undefined =>
|
|
189
|
+
delta && { tone: DELTA_TONES[delta.tone], icon: DELTA_ICONS[delta.direction], text: delta.text };
|
|
190
|
+
|
|
191
|
+
/** The loading or unavailable message, centered over the room the view reserved. */
|
|
192
|
+
function ViewStatus(props: { loading: boolean }) {
|
|
193
|
+
const locale = useLocale();
|
|
194
|
+
const t = () => aiChatMessages(locale());
|
|
195
|
+
return (
|
|
196
|
+
<div role="status" class="flex h-full items-center justify-center text-center text-xs text-dimmed">
|
|
197
|
+
{props.loading ? t().viewLoading : t().viewUnavailable}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Key figures as stat tiles. The grid's column ladder decides the layout; every tile's height follows from whether it
|
|
204
|
+
* has a sparkline and a support line, which the summary records, so a hidden copy of the same grid reserves the exact
|
|
205
|
+
* room while the file loads or when it is unavailable.
|
|
206
|
+
*/
|
|
207
|
+
function CloudStatsView(props: { summary: StatsSummary; input: CloudAiStatsView | null | undefined }) {
|
|
208
|
+
const locale = useLocale();
|
|
209
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
210
|
+
const tiles = createMemo(() => (props.input ? cloudAiStatsTiles(props.input, locale()) : null));
|
|
211
|
+
const columns = () => STATS_COLUMNS[props.summary.items.length - 1];
|
|
212
|
+
return (
|
|
213
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
214
|
+
<ViewHeading
|
|
215
|
+
id={id}
|
|
216
|
+
title={props.input?.title ?? props.summary.title}
|
|
217
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
218
|
+
/>
|
|
219
|
+
<Show
|
|
220
|
+
when={tiles()}
|
|
221
|
+
fallback={
|
|
222
|
+
<div class="relative min-w-0">
|
|
223
|
+
<div class="invisible" aria-hidden="true">
|
|
224
|
+
<StatGrid columns={columns()}>
|
|
225
|
+
<Index each={props.summary.items}>
|
|
226
|
+
{(item) => (
|
|
227
|
+
<StatCell
|
|
228
|
+
label={item().label}
|
|
229
|
+
value="0"
|
|
230
|
+
trend={item().sparkline ? [0, 1] : undefined}
|
|
231
|
+
sub={item().sub ? "\u00a0" : undefined}
|
|
232
|
+
accent={item().delta ? { tone: "zinc", icon: DELTA_ICONS.flat, text: "0" } : undefined}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
</Index>
|
|
236
|
+
</StatGrid>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="absolute inset-0">
|
|
239
|
+
<ViewStatus loading={props.input === undefined} />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
{(value) => (
|
|
245
|
+
<StatGrid columns={columns()}>
|
|
246
|
+
<Index each={value()}>
|
|
247
|
+
{(tile) => (
|
|
248
|
+
<StatCell
|
|
249
|
+
label={tile().label}
|
|
250
|
+
value={tile().value}
|
|
251
|
+
title={tile().value}
|
|
252
|
+
trend={tile().sparkline}
|
|
253
|
+
sub={tile().sub}
|
|
254
|
+
accent={deltaAccent(tile().delta)}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
</Index>
|
|
258
|
+
</StatGrid>
|
|
259
|
+
)}
|
|
260
|
+
</Show>
|
|
261
|
+
</section>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The height of the twelve one-line rows the viewport shows at most before it scrolls. */
|
|
266
|
+
const DETAILS_MAX_REM = CLOUD_AI_DETAILS_VIEWPORT_ITEMS * 2.25;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stand-in text of a value's length. Words of four digits wrap about as early as real words, and a digit is a little
|
|
270
|
+
* wider than an average letter, so the stand-in usually takes the lines of the value it stands for or one more: a row
|
|
271
|
+
* may keep a little room below it rather than hide its last line behind a scroll. Five-digit words reserved up to three
|
|
272
|
+
* extra lines in fonts whose digits are much wider than their letters, as WebKit's fallback font.
|
|
273
|
+
*/
|
|
274
|
+
const placeholder = (chars: number) =>
|
|
275
|
+
"0000 "
|
|
276
|
+
.repeat(Math.ceil(chars / 5))
|
|
277
|
+
.slice(0, chars)
|
|
278
|
+
.trimEnd();
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* One record as label and value rows. A hidden copy of the first twelve fields, with each label and a stand-in of each
|
|
282
|
+
* value's length from the summary, sizes the viewport, up to twelve one-line rows, so a value that wraps on a phone
|
|
283
|
+
* keeps its room in the first frame. More fields, or a value longer than its stand-in, scroll inside the viewport
|
|
284
|
+
* instead of moving the chat.
|
|
285
|
+
*/
|
|
286
|
+
function CloudDetailsView(props: { summary: DetailsSummary; input: CloudAiDetailsView | null | undefined }) {
|
|
287
|
+
const locale = useLocale();
|
|
288
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
289
|
+
const rows = createMemo(() => (props.input ? cloudAiDetailsRows(props.input, locale()) : null));
|
|
290
|
+
return (
|
|
291
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
292
|
+
<ViewHeading
|
|
293
|
+
id={id}
|
|
294
|
+
title={props.input?.title ?? props.summary.title}
|
|
295
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
296
|
+
/>
|
|
297
|
+
<div class="relative min-w-0 overflow-hidden" style={{ "max-height": `${DETAILS_MAX_REM}rem` }}>
|
|
298
|
+
<div class="invisible" aria-hidden="true">
|
|
299
|
+
<DescriptionList
|
|
300
|
+
layout="rows"
|
|
301
|
+
items={props.summary.fields.map((field) => ({
|
|
302
|
+
term: field.label,
|
|
303
|
+
description: !field.chars ? "—" : field.ref ? <ReferenceText text={placeholder(field.chars)} /> : placeholder(field.chars),
|
|
304
|
+
}))}
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="absolute inset-0 overflow-y-auto">
|
|
308
|
+
<Show when={rows()} fallback={<ViewStatus loading={props.input === undefined} />}>
|
|
309
|
+
{(value) => (
|
|
310
|
+
<DescriptionList
|
|
311
|
+
layout="rows"
|
|
312
|
+
items={value().map((row) => ({
|
|
313
|
+
term: row.label,
|
|
314
|
+
description: row.ref ? (
|
|
315
|
+
<ReferenceText text={row.text} reference={row.ref} />
|
|
316
|
+
) : (
|
|
317
|
+
row.text || <span class="text-dimmed">—</span>
|
|
318
|
+
),
|
|
319
|
+
}))}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
</Show>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</section>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
type Heading = { title: string; subtitle?: string };
|
|
330
|
+
const sameHeading = (a: Heading, b: Heading | undefined) => a.title === b?.title && a.subtitle === b.subtitle;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Whether a loaded file still has everything its summary reserved: kinds, headings, layout and the linked cursor. A
|
|
334
|
+
* changed file shows unavailable instead of moving views or linking charts its summary did not.
|
|
335
|
+
*/
|
|
336
|
+
const matches = (view: CloudAiView, summary: CloudAiViewSummary) => {
|
|
337
|
+
if (view.view === "table") return summary.view === "table" && sameHeading(view, summary);
|
|
338
|
+
const loaded = cloudAiViewSummary(view);
|
|
339
|
+
// A record must keep the field count and the labels and value lengths that sized its viewport.
|
|
340
|
+
if (loaded.view === "details")
|
|
341
|
+
return (
|
|
342
|
+
summary.view === "details" &&
|
|
343
|
+
sameHeading(loaded, summary) &&
|
|
344
|
+
loaded.items === summary.items &&
|
|
345
|
+
JSON.stringify(loaded.fields) === JSON.stringify(summary.fields)
|
|
346
|
+
);
|
|
347
|
+
// A stats file must keep every tile's label and parts, which set the tile heights its summary reserved.
|
|
348
|
+
if (loaded.view === "stats")
|
|
349
|
+
return summary.view === "stats" && sameHeading(loaded, summary) && JSON.stringify(loaded.items) === JSON.stringify(summary.items);
|
|
350
|
+
if (loaded.view === "explorer")
|
|
351
|
+
return (
|
|
352
|
+
summary.view === "explorer" &&
|
|
353
|
+
sameHeading(loaded, summary) &&
|
|
354
|
+
loaded.step === summary.step &&
|
|
355
|
+
loaded.filter === summary.filter &&
|
|
356
|
+
loaded.compare === summary.compare &&
|
|
357
|
+
JSON.stringify(loaded.views) === JSON.stringify(summary.views)
|
|
358
|
+
);
|
|
359
|
+
if (loaded.view === "chart") return summary.view === "chart" && loaded.kind === summary.kind && sameHeading(loaded, summary);
|
|
360
|
+
return (
|
|
361
|
+
loaded.view === "group" &&
|
|
362
|
+
summary.view === "group" &&
|
|
363
|
+
sameHeading(loaded, summary) &&
|
|
364
|
+
loaded.linkX === summary.linkX &&
|
|
365
|
+
loaded.layout === summary.layout &&
|
|
366
|
+
loaded.charts.length === summary.charts.length &&
|
|
367
|
+
loaded.charts.every((chart, index) => chart.kind === summary.charts[index]?.kind && sameHeading(chart, summary.charts[index]))
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A presented view file. It takes its full size from the `present` summary in the first frame, then loads the file
|
|
373
|
+
* with the viewer's access; a missing file or one that no longer matches its summary shows the unavailable state in
|
|
374
|
+
* the same frame.
|
|
375
|
+
*/
|
|
376
|
+
export function CloudViewBlock(props: { path: string; summary: CloudAiViewSummary }) {
|
|
377
|
+
const actions = useAiChatActions();
|
|
378
|
+
// undefined while loading, null when unavailable.
|
|
379
|
+
const [view, setView] = createSignal<CloudAiView | null | undefined>(undefined);
|
|
380
|
+
onMount(() => {
|
|
381
|
+
const url = actions.fileUrl?.(props.path);
|
|
382
|
+
if (!url) return setView(null);
|
|
383
|
+
const abort = new AbortController();
|
|
384
|
+
onCleanup(() => abort.abort());
|
|
385
|
+
void loadView(url, props.path, abort.signal).then((loaded) => {
|
|
386
|
+
if (!abort.signal.aborted) setView(loaded && matches(loaded, props.summary) ? loaded : null);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
const chartSummary = (): ChartSummary | undefined => (props.summary.view === "chart" ? props.summary : undefined);
|
|
390
|
+
const tableSummary = (): TableSummary | undefined => (props.summary.view === "table" ? props.summary : undefined);
|
|
391
|
+
const groupSummary = (): GroupSummary | undefined => (props.summary.view === "group" ? props.summary : undefined);
|
|
392
|
+
const statsSummary = (): StatsSummary | undefined => (props.summary.view === "stats" ? props.summary : undefined);
|
|
393
|
+
const detailsSummary = (): DetailsSummary | undefined => (props.summary.view === "details" ? props.summary : undefined);
|
|
394
|
+
const explorerSummary = (): ExplorerSummary | undefined => (props.summary.view === "explorer" ? props.summary : undefined);
|
|
395
|
+
function loaded(kind: "chart"): Extract<CloudAiView, { view: "chart" }> | null | undefined;
|
|
396
|
+
function loaded(kind: "table"): CloudAiTableView | null | undefined;
|
|
397
|
+
function loaded(kind: "group"): Extract<CloudAiView, { view: "group" }> | null | undefined;
|
|
398
|
+
function loaded(kind: "stats"): CloudAiStatsView | null | undefined;
|
|
399
|
+
function loaded(kind: "details"): CloudAiDetailsView | null | undefined;
|
|
400
|
+
function loaded(kind: "explorer"): CloudAiExplorerView | null | undefined;
|
|
401
|
+
function loaded(kind: CloudAiView["view"]) {
|
|
402
|
+
const value = view();
|
|
403
|
+
return value && value.view !== kind ? null : value;
|
|
404
|
+
}
|
|
405
|
+
return (
|
|
406
|
+
<div data-view-path={props.path} class="min-w-0">
|
|
407
|
+
<Switch>
|
|
408
|
+
<Match when={chartSummary()}>{(summary) => <CloudChartView summary={summary()} input={loaded("chart")} />}</Match>
|
|
409
|
+
<Match when={tableSummary()}>{(summary) => <CloudTableView summary={summary()} input={loaded("table")} />}</Match>
|
|
410
|
+
<Match when={groupSummary()}>{(summary) => <CloudChartGroupView summary={summary()} group={loaded("group")} />}</Match>
|
|
411
|
+
<Match when={statsSummary()}>{(summary) => <CloudStatsView summary={summary()} input={loaded("stats")} />}</Match>
|
|
412
|
+
<Match when={detailsSummary()}>{(summary) => <CloudDetailsView summary={summary()} input={loaded("details")} />}</Match>
|
|
413
|
+
<Match when={explorerSummary()}>{(summary) => <CloudExplorerView summary={summary()} input={loaded("explorer")} />}</Match>
|
|
414
|
+
</Switch>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
@@ -1,77 +1,56 @@
|
|
|
1
1
|
import { mutation } from "@k2b/stdlib/solid";
|
|
2
|
-
import { Button, MarkdownEditor, prompts, Slider, useLocale } from "@k2b/ui";
|
|
2
|
+
import { Button, MarkdownEditor, prompts, Slider, StatCell, type StatCellAccent, StatGrid, useLocale } from "@k2b/ui";
|
|
3
3
|
import { createSignal, createUniqueId, For, Show } from "solid-js";
|
|
4
4
|
import { CLOUD_AI_TEXT_EDITOR_FEEDBACK_MAX_CHARS, CLOUD_AI_TEXT_EDITOR_MAX_CHARS } from "../default-tool-contracts";
|
|
5
5
|
import { isRecord, jsonPreview } from "./message-utils";
|
|
6
6
|
import { aiChatMessages } from "./messages";
|
|
7
7
|
import { useAiToolDisclosure } from "./tool-disclosure";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return "border-amber-200 bg-amber-50/70 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/25 dark:text-amber-100";
|
|
15
|
-
if (tone === "red") return "border-red-200 bg-red-50/70 text-red-950 dark:border-red-900/70 dark:bg-red-950/25 dark:text-red-100";
|
|
16
|
-
if (tone === "teal") return "border-cyan-200 bg-teal-50/70 text-cyan-950 dark:border-cyan-900/70 dark:bg-cyan-950/25 dark:text-cyan-100";
|
|
17
|
-
return "border-zinc-200 bg-white text-primary dark:border-zinc-800 dark:bg-zinc-900";
|
|
18
|
-
};
|
|
19
|
-
|
|
9
|
+
/**
|
|
10
|
+
* One `card` tool call as a single stat tile, the same look as key figures in views. Its arguments stay as they
|
|
11
|
+
* were; the card has no better or worse direction, so a trend stays neutral and `tone` no longer colors anything.
|
|
12
|
+
* Nothing compares the tile with a neighbor, so its text wraps: older cards with a long value or caption stay whole.
|
|
13
|
+
*/
|
|
20
14
|
export function CloudCardBlock(props: { args: unknown }) {
|
|
21
15
|
const card = () => (isRecord(props.args) ? props.args : null);
|
|
22
16
|
const emoji = () => (typeof card()?.emoji === "string" ? String(card()?.emoji).trim() : "");
|
|
23
17
|
const title = () => String(card()?.title ?? "Card");
|
|
24
18
|
const value = () => String(card()?.value ?? "");
|
|
25
19
|
const caption = () => (typeof card()?.caption === "string" ? String(card()!.caption) : "");
|
|
26
|
-
const legacyTrend = () =>
|
|
20
|
+
const legacyTrend = () => {
|
|
21
|
+
const trend = card()?.trend;
|
|
22
|
+
return isRecord(trend) ? trend : null;
|
|
23
|
+
};
|
|
27
24
|
const trendValue = () => (typeof card()?.trendValue === "string" ? String(card()!.trendValue) : String(legacyTrend()?.value ?? ""));
|
|
28
25
|
const trendLabel = () => (typeof card()?.trendLabel === "string" ? String(card()!.trendLabel) : String(legacyTrend()?.label ?? ""));
|
|
29
26
|
const trendDirection = () => {
|
|
30
27
|
const direction = card()?.trendDirection ?? legacyTrend()?.direction;
|
|
31
28
|
return direction === "up" || direction === "down" || direction === "flat" ? direction : "flat";
|
|
32
29
|
};
|
|
33
|
-
const
|
|
30
|
+
const accent = (): StatCellAccent | undefined => {
|
|
31
|
+
const text = [trendValue(), trendLabel()].filter(Boolean).join(" ");
|
|
32
|
+
if (!text) return undefined;
|
|
33
|
+
const direction = trendDirection();
|
|
34
|
+
return {
|
|
35
|
+
tone: "zinc",
|
|
36
|
+
icon: direction === "up" ? "ti ti-trending-up" : direction === "down" ? "ti ti-trending-down" : "ti ti-minus",
|
|
37
|
+
text,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{emoji()}
|
|
49
|
-
</span>
|
|
50
|
-
</Show>
|
|
51
|
-
<div class="min-w-0 flex-1">
|
|
52
|
-
<p class="text-sm font-semibold">{title()}</p>
|
|
53
|
-
<p class="mt-2 text-3xl font-semibold tracking-normal">{value()}</p>
|
|
54
|
-
<Show when={hasTrend()}>
|
|
55
|
-
<p class="mt-1 inline-flex items-center gap-1 rounded-md bg-white/55 px-1.5 py-0.5 text-xs dark:bg-white/10">
|
|
56
|
-
<i
|
|
57
|
-
class={`ti ${
|
|
58
|
-
trendDirection() === "up" ? "ti-trending-up" : trendDirection() === "down" ? "ti-trending-down" : "ti-minus"
|
|
59
|
-
} text-sm`}
|
|
60
|
-
aria-hidden="true"
|
|
61
|
-
/>
|
|
62
|
-
<Show when={trendValue()}>{trendValue()}</Show>
|
|
63
|
-
<Show when={trendLabel()}>
|
|
64
|
-
<span class="opacity-70">{trendLabel()}</span>
|
|
65
|
-
</Show>
|
|
66
|
-
</p>
|
|
67
|
-
</Show>
|
|
68
|
-
<Show when={caption()}>
|
|
69
|
-
<p class="mt-2 text-xs opacity-70">{caption()}</p>
|
|
70
|
-
</Show>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</Show>
|
|
74
|
-
</div>
|
|
42
|
+
<Show
|
|
43
|
+
when={card()}
|
|
44
|
+
fallback={
|
|
45
|
+
<pre class="max-h-52 overflow-auto rounded-md bg-zinc-950/5 p-2 text-xs text-primary dark:bg-white/5">
|
|
46
|
+
{jsonPreview(props.args)}
|
|
47
|
+
</pre>
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
<StatGrid columns={1} class="max-w-sm">
|
|
51
|
+
<StatCell label={emoji() ? `${emoji()} ${title()}` : title()} value={value()} sub={caption() || undefined} accent={accent()} wrap />
|
|
52
|
+
</StatGrid>
|
|
53
|
+
</Show>
|
|
75
54
|
);
|
|
76
55
|
}
|
|
77
56
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Chat, ScrollArea } from "@k2b/ui";
|
|
1
|
+
import { Button, Chat, ScrollArea, useLocale } from "@k2b/ui";
|
|
2
2
|
import { createSignal, For, Show } from "solid-js";
|
|
3
3
|
import { formatAiFileSize } from "../attachments";
|
|
4
4
|
import type { AiTurnBlock } from "../protocol";
|
|
5
|
+
import { useAiChatActions } from "./message-actions";
|
|
5
6
|
import { isRecord } from "./message-utils";
|
|
7
|
+
import { aiChatMessages } from "./messages";
|
|
6
8
|
import { AiToolActivity } from "./tool-disclosure";
|
|
7
9
|
|
|
8
10
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -200,3 +202,95 @@ export function FetchFileToolBlock(props: { block: ToolBlock }) {
|
|
|
200
202
|
</Show>
|
|
201
203
|
);
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
type WebsiteReceipt = { method: string; url: string };
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Requests a website approval for this chat let through without asking, from a code run or a page read. The call
|
|
210
|
+
* reports them as they go out, so they are there whether it succeeded, failed, or its turn stopped.
|
|
211
|
+
*/
|
|
212
|
+
export const websiteReceipts = (block: ToolBlock): WebsiteReceipt[] => block.receipts ?? [];
|
|
213
|
+
|
|
214
|
+
const originOf = (url: string): string => {
|
|
215
|
+
try {
|
|
216
|
+
return new URL(url).origin;
|
|
217
|
+
} catch {
|
|
218
|
+
return url;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The receipt of every request a website approval let through: one row per website with each full URL, query
|
|
224
|
+
* included, and a revoke action that makes the next request ask again.
|
|
225
|
+
*/
|
|
226
|
+
export function WebsiteReceipts(props: { block: ToolBlock }) {
|
|
227
|
+
const actions = useAiChatActions();
|
|
228
|
+
const locale = useLocale();
|
|
229
|
+
const t = () => aiChatMessages(locale());
|
|
230
|
+
const groups = () => {
|
|
231
|
+
const byOrigin = new Map<string, WebsiteReceipt[]>();
|
|
232
|
+
for (const receipt of websiteReceipts(props.block)) {
|
|
233
|
+
const origin = originOf(receipt.url);
|
|
234
|
+
byOrigin.set(origin, [...(byOrigin.get(origin) ?? []), receipt]);
|
|
235
|
+
}
|
|
236
|
+
return [...byOrigin].map(([origin, receipts]) => ({ origin, receipts }));
|
|
237
|
+
};
|
|
238
|
+
return (
|
|
239
|
+
<For each={groups()}>
|
|
240
|
+
{(group) => {
|
|
241
|
+
const [state, setState] = createSignal<"idle" | "busy" | "revoked" | "failed">("idle");
|
|
242
|
+
const host = () => domainOf(group.origin);
|
|
243
|
+
const revoke = async () => {
|
|
244
|
+
if (!actions.onRevokeWebsite || state() === "busy") return;
|
|
245
|
+
setState("busy");
|
|
246
|
+
try {
|
|
247
|
+
await actions.onRevokeWebsite(group.origin);
|
|
248
|
+
setState("revoked");
|
|
249
|
+
} catch {
|
|
250
|
+
setState("failed");
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return (
|
|
254
|
+
<Chat.Activity
|
|
255
|
+
icon="ti ti-world-check"
|
|
256
|
+
label={host()}
|
|
257
|
+
description={
|
|
258
|
+
state() === "revoked"
|
|
259
|
+
? t().websiteRevoked
|
|
260
|
+
: state() === "failed"
|
|
261
|
+
? t().revokeFailed
|
|
262
|
+
: t().websiteAllowedForChat({ count: group.receipts.length })
|
|
263
|
+
}
|
|
264
|
+
defaultOpen
|
|
265
|
+
bodyInset={false}
|
|
266
|
+
trailing={
|
|
267
|
+
// The button keeps its place and label once revoked, so nothing moves; the description says it is revoked.
|
|
268
|
+
<Show when={actions.onRevokeWebsite}>
|
|
269
|
+
<Button
|
|
270
|
+
size="xs"
|
|
271
|
+
variant="ghost"
|
|
272
|
+
loading={state() === "busy"}
|
|
273
|
+
disabled={state() === "revoked"}
|
|
274
|
+
aria-label={t().revokeWebsiteFor({ host: host() })}
|
|
275
|
+
onClick={() => void revoke()}
|
|
276
|
+
>
|
|
277
|
+
{t().revoke}
|
|
278
|
+
</Button>
|
|
279
|
+
</Show>
|
|
280
|
+
}
|
|
281
|
+
>
|
|
282
|
+
<ul class="flex w-full min-w-0 flex-col gap-0.5 rounded-md bg-zinc-100/70 px-2 py-1.5 text-xs [box-shadow:var(--ui-control-recess)] dark:bg-zinc-950/70">
|
|
283
|
+
<For each={group.receipts}>
|
|
284
|
+
{(receipt) => (
|
|
285
|
+
<li class="min-w-0 break-all font-mono text-[11px] text-secondary">
|
|
286
|
+
<span class="font-semibold text-primary">{receipt.method}</span> {receipt.url}
|
|
287
|
+
</li>
|
|
288
|
+
)}
|
|
289
|
+
</For>
|
|
290
|
+
</ul>
|
|
291
|
+
</Chat.Activity>
|
|
292
|
+
);
|
|
293
|
+
}}
|
|
294
|
+
</For>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -1284,7 +1284,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1284
1284
|
conversationId: string,
|
|
1285
1285
|
turnId: string,
|
|
1286
1286
|
callId: string,
|
|
1287
|
-
action: { type: "approval_response"; approved: boolean; remember?: "always" } | { type: "tool_result"; result: unknown },
|
|
1287
|
+
action: { type: "approval_response"; approved: boolean; remember?: "chat" | "always" } | { type: "tool_result"; result: unknown },
|
|
1288
1288
|
) => {
|
|
1289
1289
|
if (isActiveConversation(conversationId) && runStatus() === "stopping") return false;
|
|
1290
1290
|
try {
|
|
@@ -1325,7 +1325,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1325
1325
|
const submitTurnAction = (
|
|
1326
1326
|
turnId: string,
|
|
1327
1327
|
callId: string,
|
|
1328
|
-
action: { type: "approval_response"; approved: boolean; remember?: "always" } | { type: "tool_result"; result: unknown },
|
|
1328
|
+
action: { type: "approval_response"; approved: boolean; remember?: "chat" | "always" } | { type: "tool_result"; result: unknown },
|
|
1329
1329
|
) => {
|
|
1330
1330
|
const conversationId = activeConversationId();
|
|
1331
1331
|
if (!conversationId) return Promise.resolve(false);
|
|
@@ -1333,7 +1333,10 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1333
1333
|
return submitTurnActionForConversation(conversationId, turnId, callId, action);
|
|
1334
1334
|
};
|
|
1335
1335
|
|
|
1336
|
-
const respondToApproval = async (
|
|
1336
|
+
const respondToApproval = async (
|
|
1337
|
+
request: { turnId: string; callId: string },
|
|
1338
|
+
input: { approved: boolean; remember?: "chat" | "always" },
|
|
1339
|
+
) => {
|
|
1337
1340
|
const conversationId = activeConversationId();
|
|
1338
1341
|
if (!conversationId) return false;
|
|
1339
1342
|
const submitted = await submitTurnAction(request.turnId, request.callId, {
|