@k2b/cloud 0.29.1 → 0.31.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 +4 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +113 -5
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +24 -5
- package/src/_internal/registry-validation.ts +5 -0
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +97 -40
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +26 -2
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +6 -2
- package/src/ai/chat/turn-layout.ts +10 -2
- package/src/ai/chat/turn-view.tsx +123 -36
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +109 -61
- 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 +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +189 -35
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +17 -1
- package/src/ai/migrate.ts +172 -14
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/runtime.ts +46 -7
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +47 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/capabilities.ts +3 -2
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +291 -101
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +30 -0
- package/src/capabilities/executions.ts +3 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/app.ts +34 -2
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +10 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +67 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/accounts/users.ts +15 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- 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/LayoutHelpTrigger.island.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/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- 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 +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AiTurnBlock, isRenderableTurnBlock } from "../protocol";
|
|
2
2
|
import type { AiStoredMessage } from "../types";
|
|
3
3
|
import { hasCapabilityTable } from "./capability-result";
|
|
4
|
-
import { isCardToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
4
|
+
import { isCardToolName, isChartToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
5
5
|
import { isFailedTool } from "./tool-groups";
|
|
6
6
|
|
|
7
7
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -35,6 +35,11 @@ export type AiTurnLayout = {
|
|
|
35
35
|
results: AiTurnResult[];
|
|
36
36
|
/** Place 3: the newest text. Live it is a status; finished it is the final message. */
|
|
37
37
|
text: TextBlock[];
|
|
38
|
+
/**
|
|
39
|
+
* Place 3 is only a status: the turn runs and a step started after its text. A trailing text may still be the answer
|
|
40
|
+
* and keeps answer typography; it turns quiet when the next step starts, together with the work line's new label.
|
|
41
|
+
*/
|
|
42
|
+
textIsStatus: boolean;
|
|
38
43
|
/** Place 4: effects in Cloud and decisions, open as a card or finished as a receipt. */
|
|
39
44
|
actions: AiTurnAction[];
|
|
40
45
|
/** The newest block, which names the current step of a live turn. */
|
|
@@ -69,7 +74,7 @@ const isResult = (block: ToolBlock, phase: AiTurnPhase, codePresentations: boole
|
|
|
69
74
|
// A running delivery reserves its place once its arguments arrived: a new version of an earlier result must find the
|
|
70
75
|
// earlier place before it takes one of its own.
|
|
71
76
|
const delivered = block.status === "completed" || (live(phase) && block.status === "running" && block.args !== undefined);
|
|
72
|
-
if (block.name === "present" || isCardToolName(block.name)) return delivered;
|
|
77
|
+
if (block.name === "present" || isCardToolName(block.name) || isChartToolName(block.name)) return delivered;
|
|
73
78
|
if (block.name === "code_present") return codePresentations && delivered;
|
|
74
79
|
return hasCapabilityTable(block);
|
|
75
80
|
};
|
|
@@ -124,6 +129,8 @@ export function layoutAiTurn(input: readonly AiTurnBlock[], options: { phase: Ai
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
const visible = new Set(text);
|
|
132
|
+
const lastStep = blocks.findLastIndex((block) => block.kind === "tool" || block.kind === "compaction");
|
|
133
|
+
const shownAt = text.length > 0 ? blocks.indexOf(text.at(-1)!) : -1;
|
|
127
134
|
const work = blocks.filter((block) => !visible.has(block as TextBlock));
|
|
128
135
|
|
|
129
136
|
const waits = tools.filter(waitsForUser);
|
|
@@ -135,6 +142,7 @@ export function layoutAiTurn(input: readonly AiTurnBlock[], options: { phase: Ai
|
|
|
135
142
|
showWork: steps > 0 || ((options.phase === "stopped" || options.phase === "failed") && work.length > 0),
|
|
136
143
|
results,
|
|
137
144
|
text,
|
|
145
|
+
textIsStatus: live(options.phase) && shownAt >= 0 && shownAt < lastStep,
|
|
138
146
|
actions,
|
|
139
147
|
current: blocks.at(-1) ?? null,
|
|
140
148
|
waitingFor,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { markdown } from "../../shared";
|
|
18
18
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
19
|
import type { AiTurnError } from "../types";
|
|
20
|
+
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
20
21
|
import {
|
|
21
22
|
type AiApprovalDecision,
|
|
22
23
|
ApprovalBlockView,
|
|
@@ -26,9 +27,18 @@ import {
|
|
|
26
27
|
TextEditorToolView,
|
|
27
28
|
} from "./blocks";
|
|
28
29
|
import { CapabilityTablePreview } from "./capability-table";
|
|
30
|
+
import { CloudChartBlock } from "./chart-block";
|
|
29
31
|
import { PresentToolBlock } from "./file-tools";
|
|
30
32
|
import { useAiChatActions } from "./message-actions";
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
aiToolIcon,
|
|
35
|
+
capabilityErrorDescription,
|
|
36
|
+
displayToolName,
|
|
37
|
+
isCardToolName,
|
|
38
|
+
isChartToolName,
|
|
39
|
+
isRecord,
|
|
40
|
+
isSurveyToolName,
|
|
41
|
+
} from "./message-utils";
|
|
32
42
|
import { aiChatMessages } from "./messages";
|
|
33
43
|
import { AssistantMarkdownBlock } from "./primitives";
|
|
34
44
|
import { AiToolActivity, AiToolDisclosureProvider, type AiToolDisclosureState } from "./tool-disclosure";
|
|
@@ -133,7 +143,8 @@ const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean
|
|
|
133
143
|
if (name === "present") return withTarget(t.stepDeliveringFile);
|
|
134
144
|
if (name === "code_present") return withTarget(t.stepPreparingView);
|
|
135
145
|
if (isCardToolName(name)) return t.stepPreparingCard;
|
|
136
|
-
if (
|
|
146
|
+
if (isChartToolName(name)) return t.stepPreparingChart;
|
|
147
|
+
if (["code_write", "code_import", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
137
148
|
return withTarget(t.stepWritingCode);
|
|
138
149
|
if (["code_run", "code_action", "code_sql"].includes(name)) return withTarget(t.stepRunningCode);
|
|
139
150
|
if (name === "code_check") return t.stepCheckingApp;
|
|
@@ -225,6 +236,7 @@ function AiWorkLine(props: { segment: Accessor<AiTurnSegment> }) {
|
|
|
225
236
|
tone={working() ? "ai" : "neutral"}
|
|
226
237
|
busy={working()}
|
|
227
238
|
trailing={trailing()}
|
|
239
|
+
bodyInset={false}
|
|
228
240
|
renderBody={() => <AiWorkSteps blocks={layout().work} active={working()} />}
|
|
229
241
|
/>
|
|
230
242
|
);
|
|
@@ -264,7 +276,27 @@ function WorkEntryRow(props: { entry: AiWorkEntry; busy: boolean }) {
|
|
|
264
276
|
);
|
|
265
277
|
}
|
|
266
278
|
|
|
267
|
-
/**
|
|
279
|
+
/** The steps of an opened group, keyed by block id, so a row keeps its element and focus when its block is replaced. */
|
|
280
|
+
function WorkEntries(props: { entries: AiWorkEntry[]; active: boolean }) {
|
|
281
|
+
const entries = createMemo(() => new Map(props.entries.map((entry) => [entry.id, entry])));
|
|
282
|
+
const ids = createMemo(() => [...entries().keys()]);
|
|
283
|
+
return (
|
|
284
|
+
<div class="ai-turn-steps">
|
|
285
|
+
<For each={ids()}>
|
|
286
|
+
{(id, index) => (
|
|
287
|
+
<Show when={entries().get(id)}>
|
|
288
|
+
{(entry) => <WorkEntryRow entry={entry()} busy={props.active && index() === ids().length - 1} />}
|
|
289
|
+
</Show>
|
|
290
|
+
)}
|
|
291
|
+
</For>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Housekeeping folded into one row at step level, with its count at the end like the work line's. It opens in place:
|
|
298
|
+
* its steps follow at the same indent.
|
|
299
|
+
*/
|
|
268
300
|
function HousekeepingGroup(props: { id: string; entries: AiWorkEntry[]; active: boolean }) {
|
|
269
301
|
const locale = useLocale();
|
|
270
302
|
const last = () => props.entries.at(-1)!;
|
|
@@ -272,17 +304,33 @@ function HousekeepingGroup(props: { id: string; entries: AiWorkEntry[]; active:
|
|
|
272
304
|
<AiToolActivity
|
|
273
305
|
blockId={`group:${props.id}`}
|
|
274
306
|
label={aiChatMessages(locale()).groupLoad}
|
|
275
|
-
|
|
307
|
+
trailing={<span class="ai-turn-work__meta">{countWorkSteps(props.entries, locale())}</span>}
|
|
276
308
|
icon="ti ti-stack-2"
|
|
277
309
|
bodyInset={false}
|
|
278
310
|
busy={props.active && last().kind === "tool" && (last() as ToolBlock).status === "running"}
|
|
279
|
-
renderBody={() =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
311
|
+
renderBody={() => <WorkEntries entries={props.entries} active={props.active} />}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Consecutive instances of one call apart from paging, such as reading one file in parts, folded into one row that
|
|
318
|
+
* opens in place. Its count sits at the end like the work line's, so a narrow screen cuts the target short, never the count.
|
|
319
|
+
*/
|
|
320
|
+
function RepeatedSteps(props: { id: string; target: string; entries: AiWorkEntry[]; active: boolean }) {
|
|
321
|
+
const locale = useLocale();
|
|
322
|
+
const tools = () => props.entries.filter((entry): entry is ToolBlock => entry.kind === "tool");
|
|
323
|
+
const first = () => tools()[0]!;
|
|
324
|
+
return (
|
|
325
|
+
<AiToolActivity
|
|
326
|
+
blockId={`repeat:${props.id}`}
|
|
327
|
+
label={first().presentation?.title ?? displayToolName(first().name, locale())}
|
|
328
|
+
description={props.target}
|
|
329
|
+
trailing={<span class="ai-turn-work__meta">{aiChatMessages(locale()).repeatCount({ count: tools().length })}</span>}
|
|
330
|
+
icon={aiToolIcon(first().name, first().presentation?.appIcon)}
|
|
331
|
+
bodyInset={false}
|
|
332
|
+
busy={props.active && tools().at(-1)?.status === "running"}
|
|
333
|
+
renderBody={() => <WorkEntries entries={props.entries} active={props.active} />}
|
|
286
334
|
/>
|
|
287
335
|
);
|
|
288
336
|
}
|
|
@@ -300,13 +348,19 @@ function WorkText(props: { text: string }) {
|
|
|
300
348
|
}
|
|
301
349
|
|
|
302
350
|
const workItemKey = (item: AiWorkItem): string =>
|
|
303
|
-
item.kind === "text"
|
|
351
|
+
item.kind === "text"
|
|
352
|
+
? `text:${item.block.id}`
|
|
353
|
+
: item.kind === "step"
|
|
354
|
+
? `step:${item.entry.id}`
|
|
355
|
+
: item.kind === "repeat"
|
|
356
|
+
? `repeat:${item.id}`
|
|
357
|
+
: `group:${item.id}`;
|
|
304
358
|
|
|
305
359
|
/**
|
|
306
|
-
* Expanded work,
|
|
307
|
-
* them directly, and housekeeping folded at step level. A row shown while this list is open never
|
|
308
|
-
* reader, also when reasoning or arguments that would fold it arrive later; the next time it
|
|
309
|
-
* history.
|
|
360
|
+
* Expanded work, at the work line's indent and one size: intermediate texts as quiet paragraphs, the steps between
|
|
361
|
+
* them directly, and housekeeping or repeated steps folded at step level. A row shown while this list is open never
|
|
362
|
+
* folds away under the reader, also when reasoning or arguments that would fold it arrive later; the next time it
|
|
363
|
+
* opens, it folds like in history.
|
|
310
364
|
*/
|
|
311
365
|
function AiWorkSteps(props: { blocks: AiTurnBlock[]; active: boolean }) {
|
|
312
366
|
const shown = new Set<string>();
|
|
@@ -333,12 +387,19 @@ function AiWorkSteps(props: { blocks: AiTurnBlock[]; active: boolean }) {
|
|
|
333
387
|
const value = item();
|
|
334
388
|
return value?.kind === "housekeeping" ? value : undefined;
|
|
335
389
|
};
|
|
390
|
+
const repeat = () => {
|
|
391
|
+
const value = item();
|
|
392
|
+
return value?.kind === "repeat" ? value : undefined;
|
|
393
|
+
};
|
|
336
394
|
const active = () => props.active && index() === keys().length - 1;
|
|
337
395
|
return (
|
|
338
396
|
<Switch>
|
|
339
397
|
<Match when={text()}>{(value) => <WorkText text={value()} />}</Match>
|
|
340
398
|
<Match when={step()}>{(entry) => <WorkEntryRow entry={entry()} busy={active()} />}</Match>
|
|
341
399
|
<Match when={group()}>{(value) => <HousekeepingGroup id={value().id} entries={value().entries} active={active()} />}</Match>
|
|
400
|
+
<Match when={repeat()}>
|
|
401
|
+
{(value) => <RepeatedSteps id={value().id} target={value().target} entries={value().entries} active={active()} />}
|
|
402
|
+
</Match>
|
|
342
403
|
</Switch>
|
|
343
404
|
);
|
|
344
405
|
}}
|
|
@@ -436,6 +497,9 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
436
497
|
<Match when={isCardToolName(current().name)}>
|
|
437
498
|
<CloudCardBlock args={current().args} />
|
|
438
499
|
</Match>
|
|
500
|
+
<Match when={isChartToolName(current().name)}>
|
|
501
|
+
<CloudChartBlock args={current().args} completed={current().status === "completed"} />
|
|
502
|
+
</Match>
|
|
439
503
|
<Match when={true}>
|
|
440
504
|
<Show when={!props.receipt()}>
|
|
441
505
|
<CapabilityResultRow block={current()} />
|
|
@@ -455,33 +519,37 @@ type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
|
455
519
|
|
|
456
520
|
/**
|
|
457
521
|
* One line for an action in place 4, the same element while it runs and once it is done, rejected, failed, or not run.
|
|
458
|
-
* It
|
|
459
|
-
* application's own summary of
|
|
522
|
+
* It says what happened in the owning app's words: its sentence for the outcome, such as "Moved report.pdf to the
|
|
523
|
+
* trash", else, once done, the application's own summary of the call. Without either it puts the state in the reader's
|
|
524
|
+
* language before what the call does, such as "Rejected: Write report.md", or before its title and target.
|
|
460
525
|
*/
|
|
461
526
|
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
462
527
|
const locale = useLocale();
|
|
463
528
|
const t = () => aiChatMessages(locale());
|
|
464
529
|
const block = () => props.block;
|
|
465
530
|
const subject = () => {
|
|
531
|
+
const sentence = toolSubject(block(), locale());
|
|
532
|
+
if (sentence) return sentence;
|
|
466
533
|
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
467
534
|
const target = toolTarget(block());
|
|
468
535
|
return target ? `${title} · ${target}` : title;
|
|
469
536
|
};
|
|
537
|
+
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
470
538
|
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
471
539
|
const label = () => {
|
|
472
540
|
const state = props.state;
|
|
473
541
|
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
474
542
|
if (state === "done") {
|
|
475
|
-
const summary = resultSummary(block());
|
|
476
|
-
if (summary) return summary;
|
|
477
543
|
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
478
|
-
return
|
|
479
|
-
|
|
480
|
-
: t().receiptApproved({ title: subject() });
|
|
544
|
+
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
545
|
+
return toolOutcome(block(), "done", locale()) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
481
546
|
}
|
|
482
|
-
if (state === "rejected") return t().receiptRejected({ title: subject() });
|
|
547
|
+
if (state === "rejected") return toolOutcome(block(), "rejected", locale()) ?? t().receiptRejected({ title: subject() });
|
|
483
548
|
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
484
|
-
|
|
549
|
+
const notRun = toolOutcome(block(), "notRun", locale());
|
|
550
|
+
return notRun
|
|
551
|
+
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
552
|
+
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
485
553
|
};
|
|
486
554
|
const declined = () => props.state === "rejected" || props.state === "not_run";
|
|
487
555
|
return (
|
|
@@ -529,7 +597,7 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
529
597
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
530
598
|
announce(
|
|
531
599
|
aiChatMessages(locale()).approvalNeeded({
|
|
532
|
-
title: current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
600
|
+
title: toolSubject(current.block, locale()) ?? current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
533
601
|
}),
|
|
534
602
|
);
|
|
535
603
|
}),
|
|
@@ -623,14 +691,18 @@ function AiTurnErrorNotice(props: { error: AiTurnError; continuable: boolean })
|
|
|
623
691
|
}
|
|
624
692
|
|
|
625
693
|
/**
|
|
626
|
-
* Place 3. Live it is a status that switches in this same element; it keeps its height until the turn ends.
|
|
627
|
-
*
|
|
694
|
+
* Place 3. Live it is a status that switches in this same element; it keeps its height until the turn ends. Once a step
|
|
695
|
+
* started after it, it reads quietly like the texts in the expanded work; a trailing text may still be the answer and
|
|
696
|
+
* keeps answer typography. A status is not read token by token: screen readers hear each status once it is complete,
|
|
697
|
+
* and that the answer is ready.
|
|
628
698
|
*/
|
|
629
699
|
function AiTurnText(props: {
|
|
630
700
|
blocks: Accessor<Extract<AiTurnBlock, { kind: "text" }>[]>;
|
|
631
701
|
live: Accessor<boolean>;
|
|
632
702
|
/** The turn has work steps, so its live text is a status rather than the answer itself. */
|
|
633
703
|
folding: Accessor<boolean>;
|
|
704
|
+
/** A step started after the text, so it can no longer be the answer: it reads quietly. */
|
|
705
|
+
quiet: Accessor<boolean>;
|
|
634
706
|
/** An earlier segment of a split turn: it stops being live while the turn goes on. */
|
|
635
707
|
earlier: Accessor<boolean>;
|
|
636
708
|
}) {
|
|
@@ -644,6 +716,7 @@ function AiTurnText(props: {
|
|
|
644
716
|
);
|
|
645
717
|
const html = createMemo(() => markdown.renderSync(source(), { locale: locale() }));
|
|
646
718
|
const shownId = createMemo(() => props.blocks()[0]?.id);
|
|
719
|
+
const quiet = createMemo(() => props.quiet());
|
|
647
720
|
const [minHeight, setMinHeight] = createSignal<number | null>(null);
|
|
648
721
|
let element!: HTMLDivElement;
|
|
649
722
|
let lastHeight = 0;
|
|
@@ -655,14 +728,21 @@ function AiTurnText(props: {
|
|
|
655
728
|
observer.observe(element);
|
|
656
729
|
onCleanup(() => observer.disconnect());
|
|
657
730
|
});
|
|
731
|
+
/** The quiet text's height in answer typography, measured in place before the browser paints. */
|
|
732
|
+
const answerHeight = () => {
|
|
733
|
+
element.classList.remove("ai-turn__text--status");
|
|
734
|
+
const height = element.offsetHeight;
|
|
735
|
+
element.classList.add("ai-turn__text--status");
|
|
736
|
+
return height;
|
|
737
|
+
};
|
|
738
|
+
// A new status keeps the height of the one before it. A quiet status keeps the height it has as the answer, also when
|
|
739
|
+
// it first shows quietly after a reload, so nothing below moves when it is replaced or becomes the answer again.
|
|
658
740
|
createEffect(
|
|
659
|
-
on(
|
|
660
|
-
|
|
661
|
-
()
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
{ defer: true },
|
|
665
|
-
),
|
|
741
|
+
on([shownId, quiet], ([id, isQuiet], previous) => {
|
|
742
|
+
if (!props.live()) return;
|
|
743
|
+
const height = Math.max(previous && previous[0] !== id ? lastHeight : 0, isQuiet ? answerHeight() : 0);
|
|
744
|
+
if (height > 0) setMinHeight((current) => Math.max(current ?? 0, height));
|
|
745
|
+
}),
|
|
666
746
|
);
|
|
667
747
|
createEffect(() => {
|
|
668
748
|
if (!props.live()) setMinHeight(null);
|
|
@@ -683,6 +763,7 @@ function AiTurnText(props: {
|
|
|
683
763
|
<div
|
|
684
764
|
ref={element}
|
|
685
765
|
class="ai-turn__text"
|
|
766
|
+
classList={{ "ai-turn__text--status": quiet() }}
|
|
686
767
|
aria-live={status() ? "off" : undefined}
|
|
687
768
|
style={minHeight() ? { "min-height": `${minHeight()}px` } : undefined}
|
|
688
769
|
>
|
|
@@ -739,7 +820,13 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
|
|
|
739
820
|
</Show>
|
|
740
821
|
<For each={resultIds()}>{(id) => <AiTurnResultView result={() => results().get(id)} receipt={() => actions().has(id)} />}</For>
|
|
741
822
|
<Show when={layout().text.length > 0}>
|
|
742
|
-
<AiTurnText
|
|
823
|
+
<AiTurnText
|
|
824
|
+
blocks={() => layout().text}
|
|
825
|
+
live={live}
|
|
826
|
+
folding={() => layout().steps > 0}
|
|
827
|
+
quiet={() => layout().textIsStatus}
|
|
828
|
+
earlier={() => props.segment().earlier}
|
|
829
|
+
/>
|
|
743
830
|
</Show>
|
|
744
831
|
<For each={actionIds()}>
|
|
745
832
|
{(id) => <AiTurnActionView action={() => actions().get(id)} turnId={props.segment().turnId} phase={props.segment().phase} />}
|
|
@@ -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, {
|