@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
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { aiSkillFilePathFromMount } from "../file-mount";
|
|
2
2
|
import type { AiTurnBlock } from "../protocol";
|
|
3
|
+
import { isRecord } from "./message-utils";
|
|
3
4
|
import { aiChatMessages } from "./messages";
|
|
4
5
|
|
|
5
6
|
type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
6
7
|
type Text = Extract<AiTurnBlock, { kind: "text" }>;
|
|
7
8
|
export type AiWorkEntry = Exclude<AiTurnBlock, { kind: "text" } | { kind: "steer_message" }>;
|
|
8
9
|
/**
|
|
9
|
-
* One row of the expanded work
|
|
10
|
-
*
|
|
10
|
+
* One row of the expanded work: an intermediate text, a step, or a group that folds housekeeping or repeated calls
|
|
11
|
+
* on one target and opens in place.
|
|
11
12
|
*/
|
|
12
13
|
export type AiWorkItem =
|
|
13
14
|
| { kind: "text"; block: Text }
|
|
14
15
|
| { kind: "step"; entry: AiWorkEntry }
|
|
15
|
-
| { kind: "housekeeping"; id: string; entries: AiWorkEntry[] }
|
|
16
|
+
| { kind: "housekeeping"; id: string; entries: AiWorkEntry[] }
|
|
17
|
+
| { kind: "repeat"; id: string; target: string; entries: AiWorkEntry[] };
|
|
16
18
|
|
|
17
19
|
/** A failed call, including a code run that reported an error. A rejected approval is a decision, not a failure. */
|
|
18
20
|
export function isFailedTool(tool: Tool): boolean {
|
|
@@ -30,6 +32,7 @@ export function isFailedTool(tool: Tool): boolean {
|
|
|
30
32
|
|
|
31
33
|
const HOUSEKEEPING_TOOLS = new Set([
|
|
32
34
|
"load_skill",
|
|
35
|
+
"load_local_skill",
|
|
33
36
|
"search_skills",
|
|
34
37
|
"load_tools",
|
|
35
38
|
"search_tools",
|
|
@@ -50,33 +53,85 @@ export const isHousekeepingTool = (tool: Tool): boolean => {
|
|
|
50
53
|
|
|
51
54
|
const isStep = (entry: AiWorkEntry) => entry.kind === "tool" || entry.kind === "compaction";
|
|
52
55
|
|
|
56
|
+
/** The file, address, or query a folded row shows. A call without one never folds with others. */
|
|
57
|
+
const callTarget = (tool: Tool): string | undefined => {
|
|
58
|
+
const args = isRecord(tool.args) ? tool.args : {};
|
|
59
|
+
return [args.path, args.url, args.query].find((value): value is string => typeof value === "string" && value.trim() !== "")?.trim();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Arguments that page through one call's result, such as read_file's byte slices or an app's cursor and page size. */
|
|
63
|
+
const PAGING_ARGUMENTS = new Set(["offset", "length", "limit", "cursor", "after", "page", "pageSize"]);
|
|
64
|
+
|
|
65
|
+
type Fold = { kind: "housekeeping" } | { kind: "repeat"; name: string; target: string; identity: string };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a step folds with: housekeeping with housekeeping, a running or finished call with the same call apart from
|
|
69
|
+
* paging, such as reading one file in parts. Repeated calls need a target; failed, rejected, and waiting calls never repeat.
|
|
70
|
+
*/
|
|
71
|
+
const foldOf = (tool: Tool): Fold | null => {
|
|
72
|
+
if (isHousekeepingTool(tool)) return { kind: "housekeeping" };
|
|
73
|
+
const target = callTarget(tool);
|
|
74
|
+
if (!target || isFailedTool(tool) || (tool.status !== "completed" && tool.status !== "running")) return null;
|
|
75
|
+
const args = isRecord(tool.args) ? tool.args : {};
|
|
76
|
+
const identity = JSON.stringify(
|
|
77
|
+
Object.fromEntries(Object.entries(args).filter(([key]) => !PAGING_ARGUMENTS.has(key))),
|
|
78
|
+
(_key: string, value: unknown) =>
|
|
79
|
+
typeof value === "string"
|
|
80
|
+
? value.trim()
|
|
81
|
+
: isRecord(value)
|
|
82
|
+
? Object.fromEntries(
|
|
83
|
+
Object.keys(value)
|
|
84
|
+
.sort()
|
|
85
|
+
.map((key) => [key, value[key]]),
|
|
86
|
+
)
|
|
87
|
+
: value,
|
|
88
|
+
);
|
|
89
|
+
return { kind: "repeat", name: tool.name, target, identity };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sameFold = (a: Fold, b: Fold): boolean =>
|
|
93
|
+
a.kind === "housekeeping" ? b.kind === "housekeeping" : b.kind === "repeat" && a.name === b.name && a.identity === b.identity;
|
|
94
|
+
|
|
53
95
|
/**
|
|
54
|
-
* The rows of the expanded work, in the original order. A run of at least two
|
|
55
|
-
* between them,
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
96
|
+
* The rows of the expanded work, in the original order. A run of at least two steps that fold together, with the
|
|
97
|
+
* reasoning between them, becomes one row: housekeeping, or consecutive instances of the same call apart from paging,
|
|
98
|
+
* such as reading one file in parts. Anything else between them, such as an intermediate text, ends the run. A run that
|
|
99
|
+
* would hold all but one of the work's steps shows its steps directly, so the work line never opens to a summary of
|
|
100
|
+
* itself. Entries listed in `shown` already show as rows of their own and never fold, so nothing a reader sees folds
|
|
101
|
+
* away under them.
|
|
102
|
+
* A folded row is keyed by its first tool, whose id is the same live and in history.
|
|
59
103
|
*/
|
|
60
104
|
export function groupWorkBlocks(blocks: readonly AiTurnBlock[], shown: ReadonlySet<string> = new Set()): AiWorkItem[] {
|
|
61
105
|
const entries = blocks.filter((block): block is AiWorkEntry | Text => block.kind !== "steer_message");
|
|
62
106
|
const steps = entries.filter((entry) => entry.kind !== "text" && isStep(entry)).length;
|
|
63
107
|
const items: AiWorkItem[] = [];
|
|
108
|
+
let fold: Fold | null = null;
|
|
64
109
|
let run: AiWorkEntry[] = [];
|
|
65
110
|
let trailing: AiWorkEntry[] = [];
|
|
66
111
|
const flush = () => {
|
|
67
|
-
if (run.length > 0) {
|
|
112
|
+
if (fold && run.length > 0) {
|
|
68
113
|
const runSteps = run.filter(isStep).length;
|
|
69
|
-
|
|
114
|
+
const id = run[0]!.id;
|
|
115
|
+
if (runSteps >= 2 && steps - runSteps >= 2)
|
|
116
|
+
items.push(
|
|
117
|
+
fold.kind === "housekeeping"
|
|
118
|
+
? { kind: "housekeeping", id, entries: run }
|
|
119
|
+
: { kind: "repeat", id, target: fold.target, entries: run },
|
|
120
|
+
);
|
|
70
121
|
else for (const entry of run) items.push({ kind: "step", entry });
|
|
71
122
|
}
|
|
72
|
-
// Reasoning after the last
|
|
123
|
+
// Reasoning after the last folded step belongs to what comes next.
|
|
73
124
|
for (const entry of trailing) items.push({ kind: "step", entry });
|
|
125
|
+
fold = null;
|
|
74
126
|
run = [];
|
|
75
127
|
trailing = [];
|
|
76
128
|
};
|
|
77
129
|
for (const entry of entries) {
|
|
78
130
|
const foldable = !shown.has(entry.id);
|
|
79
|
-
|
|
131
|
+
const next = foldable && entry.kind === "tool" ? foldOf(entry) : null;
|
|
132
|
+
if (next && entry.kind === "tool") {
|
|
133
|
+
if (fold && !sameFold(fold, next)) flush();
|
|
134
|
+
fold = next;
|
|
80
135
|
run.push(...trailing, entry);
|
|
81
136
|
trailing = [];
|
|
82
137
|
} else if (foldable && entry.kind === "thinking" && run.length > 0) trailing.push(entry);
|
|
@@ -14,6 +14,13 @@ const toolNames = i18n.define({
|
|
|
14
14
|
survey: "Survey",
|
|
15
15
|
text_editor: "Text editor",
|
|
16
16
|
local_bash: "Local Bash",
|
|
17
|
+
local_list: "List local files",
|
|
18
|
+
local_read: "Read local file",
|
|
19
|
+
local_upload: "Upload local files",
|
|
20
|
+
local_download: "Save to local folder",
|
|
21
|
+
local_write: "Write local file",
|
|
22
|
+
local_move: "Move local files",
|
|
23
|
+
load_local_skill: "Load local Skill",
|
|
17
24
|
todo_write: "Working plan",
|
|
18
25
|
memory: "Memory",
|
|
19
26
|
list_files: "List files",
|
|
@@ -72,7 +79,8 @@ const toolNames = i18n.define({
|
|
|
72
79
|
code_restore: "Restore app version",
|
|
73
80
|
code_update: "Update app details",
|
|
74
81
|
code_create: "Create app",
|
|
75
|
-
code_write: "Save app
|
|
82
|
+
code_write: "Save app file",
|
|
83
|
+
code_import: "Import app files",
|
|
76
84
|
code_remove: "Remove app file",
|
|
77
85
|
},
|
|
78
86
|
de: {
|
|
@@ -82,6 +90,13 @@ const toolNames = i18n.define({
|
|
|
82
90
|
survey: "Umfrage",
|
|
83
91
|
text_editor: "Texteditor",
|
|
84
92
|
local_bash: "Lokale Bash",
|
|
93
|
+
local_list: "Lokale Dateien auflisten",
|
|
94
|
+
local_read: "Lokale Datei lesen",
|
|
95
|
+
local_upload: "Lokale Dateien hochladen",
|
|
96
|
+
local_download: "In lokalen Ordner speichern",
|
|
97
|
+
local_write: "Lokale Datei schreiben",
|
|
98
|
+
local_move: "Lokale Dateien verschieben",
|
|
99
|
+
load_local_skill: "Lokalen Skill laden",
|
|
85
100
|
todo_write: "Arbeitsplan",
|
|
86
101
|
memory: "Gedächtnis",
|
|
87
102
|
list_files: "Dateien auflisten",
|
|
@@ -140,7 +155,8 @@ const toolNames = i18n.define({
|
|
|
140
155
|
code_restore: "App-Version wiederherstellen",
|
|
141
156
|
code_update: "App-Details ändern",
|
|
142
157
|
code_create: "App erstellen",
|
|
143
|
-
code_write: "App-
|
|
158
|
+
code_write: "App-Datei speichern",
|
|
159
|
+
code_import: "App-Dateien importieren",
|
|
144
160
|
code_remove: "App-Datei entfernen",
|
|
145
161
|
},
|
|
146
162
|
},
|
|
@@ -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. */
|
|
@@ -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,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dates } from "@k2b/stdlib";
|
|
1
2
|
import { announce, Button, ButtonLink, Chat, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
3
|
import {
|
|
3
4
|
type Accessor,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
import { markdown } from "../../shared";
|
|
18
19
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
20
|
import type { AiTurnError } from "../types";
|
|
20
|
-
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
21
|
+
import { toolOutcome, toolSubject, useReaderTimeZone } from "./action-sentences";
|
|
21
22
|
import {
|
|
22
23
|
type AiApprovalDecision,
|
|
23
24
|
ApprovalBlockView,
|
|
@@ -144,7 +145,7 @@ const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean
|
|
|
144
145
|
if (name === "code_present") return withTarget(t.stepPreparingView);
|
|
145
146
|
if (isCardToolName(name)) return t.stepPreparingCard;
|
|
146
147
|
if (isChartToolName(name)) return t.stepPreparingChart;
|
|
147
|
-
if (["code_write", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
148
|
+
if (["code_write", "code_import", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
148
149
|
return withTarget(t.stepWritingCode);
|
|
149
150
|
if (["code_run", "code_action", "code_sql"].includes(name)) return withTarget(t.stepRunningCode);
|
|
150
151
|
if (name === "code_check") return t.stepCheckingApp;
|
|
@@ -236,6 +237,7 @@ function AiWorkLine(props: { segment: Accessor<AiTurnSegment> }) {
|
|
|
236
237
|
tone={working() ? "ai" : "neutral"}
|
|
237
238
|
busy={working()}
|
|
238
239
|
trailing={trailing()}
|
|
240
|
+
bodyInset={false}
|
|
239
241
|
renderBody={() => <AiWorkSteps blocks={layout().work} active={working()} />}
|
|
240
242
|
/>
|
|
241
243
|
);
|
|
@@ -275,7 +277,27 @@ function WorkEntryRow(props: { entry: AiWorkEntry; busy: boolean }) {
|
|
|
275
277
|
);
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
/**
|
|
280
|
+
/** The steps of an opened group, keyed by block id, so a row keeps its element and focus when its block is replaced. */
|
|
281
|
+
function WorkEntries(props: { entries: AiWorkEntry[]; active: boolean }) {
|
|
282
|
+
const entries = createMemo(() => new Map(props.entries.map((entry) => [entry.id, entry])));
|
|
283
|
+
const ids = createMemo(() => [...entries().keys()]);
|
|
284
|
+
return (
|
|
285
|
+
<div class="ai-turn-steps">
|
|
286
|
+
<For each={ids()}>
|
|
287
|
+
{(id, index) => (
|
|
288
|
+
<Show when={entries().get(id)}>
|
|
289
|
+
{(entry) => <WorkEntryRow entry={entry()} busy={props.active && index() === ids().length - 1} />}
|
|
290
|
+
</Show>
|
|
291
|
+
)}
|
|
292
|
+
</For>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Housekeeping folded into one row at step level, with its count at the end like the work line's. It opens in place:
|
|
299
|
+
* its steps follow at the same indent.
|
|
300
|
+
*/
|
|
279
301
|
function HousekeepingGroup(props: { id: string; entries: AiWorkEntry[]; active: boolean }) {
|
|
280
302
|
const locale = useLocale();
|
|
281
303
|
const last = () => props.entries.at(-1)!;
|
|
@@ -283,17 +305,33 @@ function HousekeepingGroup(props: { id: string; entries: AiWorkEntry[]; active:
|
|
|
283
305
|
<AiToolActivity
|
|
284
306
|
blockId={`group:${props.id}`}
|
|
285
307
|
label={aiChatMessages(locale()).groupLoad}
|
|
286
|
-
|
|
308
|
+
trailing={<span class="ai-turn-work__meta">{countWorkSteps(props.entries, locale())}</span>}
|
|
287
309
|
icon="ti ti-stack-2"
|
|
288
310
|
bodyInset={false}
|
|
289
311
|
busy={props.active && last().kind === "tool" && (last() as ToolBlock).status === "running"}
|
|
290
|
-
renderBody={() =>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
renderBody={() => <WorkEntries entries={props.entries} active={props.active} />}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Consecutive instances of one call apart from paging, such as reading one file in parts, folded into one row that
|
|
319
|
+
* 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.
|
|
320
|
+
*/
|
|
321
|
+
function RepeatedSteps(props: { id: string; target: string; entries: AiWorkEntry[]; active: boolean }) {
|
|
322
|
+
const locale = useLocale();
|
|
323
|
+
const tools = () => props.entries.filter((entry): entry is ToolBlock => entry.kind === "tool");
|
|
324
|
+
const first = () => tools()[0]!;
|
|
325
|
+
return (
|
|
326
|
+
<AiToolActivity
|
|
327
|
+
blockId={`repeat:${props.id}`}
|
|
328
|
+
label={first().presentation?.title ?? displayToolName(first().name, locale())}
|
|
329
|
+
description={props.target}
|
|
330
|
+
trailing={<span class="ai-turn-work__meta">{aiChatMessages(locale()).repeatCount({ count: tools().length })}</span>}
|
|
331
|
+
icon={aiToolIcon(first().name, first().presentation?.appIcon)}
|
|
332
|
+
bodyInset={false}
|
|
333
|
+
busy={props.active && tools().at(-1)?.status === "running"}
|
|
334
|
+
renderBody={() => <WorkEntries entries={props.entries} active={props.active} />}
|
|
297
335
|
/>
|
|
298
336
|
);
|
|
299
337
|
}
|
|
@@ -311,13 +349,19 @@ function WorkText(props: { text: string }) {
|
|
|
311
349
|
}
|
|
312
350
|
|
|
313
351
|
const workItemKey = (item: AiWorkItem): string =>
|
|
314
|
-
item.kind === "text"
|
|
352
|
+
item.kind === "text"
|
|
353
|
+
? `text:${item.block.id}`
|
|
354
|
+
: item.kind === "step"
|
|
355
|
+
? `step:${item.entry.id}`
|
|
356
|
+
: item.kind === "repeat"
|
|
357
|
+
? `repeat:${item.id}`
|
|
358
|
+
: `group:${item.id}`;
|
|
315
359
|
|
|
316
360
|
/**
|
|
317
|
-
* Expanded work,
|
|
318
|
-
* them directly, and housekeeping folded at step level. A row shown while this list is open never
|
|
319
|
-
* reader, also when reasoning or arguments that would fold it arrive later; the next time it
|
|
320
|
-
* history.
|
|
361
|
+
* Expanded work, at the work line's indent and one size: intermediate texts as quiet paragraphs, the steps between
|
|
362
|
+
* them directly, and housekeeping or repeated steps folded at step level. A row shown while this list is open never
|
|
363
|
+
* folds away under the reader, also when reasoning or arguments that would fold it arrive later; the next time it
|
|
364
|
+
* opens, it folds like in history.
|
|
321
365
|
*/
|
|
322
366
|
function AiWorkSteps(props: { blocks: AiTurnBlock[]; active: boolean }) {
|
|
323
367
|
const shown = new Set<string>();
|
|
@@ -344,12 +388,19 @@ function AiWorkSteps(props: { blocks: AiTurnBlock[]; active: boolean }) {
|
|
|
344
388
|
const value = item();
|
|
345
389
|
return value?.kind === "housekeeping" ? value : undefined;
|
|
346
390
|
};
|
|
391
|
+
const repeat = () => {
|
|
392
|
+
const value = item();
|
|
393
|
+
return value?.kind === "repeat" ? value : undefined;
|
|
394
|
+
};
|
|
347
395
|
const active = () => props.active && index() === keys().length - 1;
|
|
348
396
|
return (
|
|
349
397
|
<Switch>
|
|
350
398
|
<Match when={text()}>{(value) => <WorkText text={value()} />}</Match>
|
|
351
399
|
<Match when={step()}>{(entry) => <WorkEntryRow entry={entry()} busy={active()} />}</Match>
|
|
352
400
|
<Match when={group()}>{(value) => <HousekeepingGroup id={value().id} entries={value().entries} active={active()} />}</Match>
|
|
401
|
+
<Match when={repeat()}>
|
|
402
|
+
{(value) => <RepeatedSteps id={value().id} target={value().target} entries={value().entries} active={active()} />}
|
|
403
|
+
</Match>
|
|
353
404
|
</Switch>
|
|
354
405
|
);
|
|
355
406
|
}}
|
|
@@ -475,10 +526,11 @@ type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
|
475
526
|
*/
|
|
476
527
|
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
477
528
|
const locale = useLocale();
|
|
529
|
+
const timeZone = useReaderTimeZone();
|
|
478
530
|
const t = () => aiChatMessages(locale());
|
|
479
531
|
const block = () => props.block;
|
|
480
532
|
const subject = () => {
|
|
481
|
-
const sentence = toolSubject(block(), locale());
|
|
533
|
+
const sentence = toolSubject(block(), locale(), timeZone);
|
|
482
534
|
if (sentence) return sentence;
|
|
483
535
|
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
484
536
|
const target = toolTarget(block());
|
|
@@ -486,22 +538,26 @@ function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: bool
|
|
|
486
538
|
};
|
|
487
539
|
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
488
540
|
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
489
|
-
const
|
|
541
|
+
const outcomeLabel = () => {
|
|
490
542
|
const state = props.state;
|
|
491
543
|
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
492
544
|
if (state === "done") {
|
|
493
545
|
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
494
546
|
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
495
|
-
return toolOutcome(block(), "done", locale()) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
547
|
+
return toolOutcome(block(), "done", locale(), timeZone) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
496
548
|
}
|
|
497
|
-
if (state === "rejected") return toolOutcome(block(), "rejected", locale()) ?? t().receiptRejected({ title: subject() });
|
|
549
|
+
if (state === "rejected") return toolOutcome(block(), "rejected", locale(), timeZone) ?? t().receiptRejected({ title: subject() });
|
|
498
550
|
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
499
|
-
const notRun = toolOutcome(block(), "notRun", locale());
|
|
551
|
+
const notRun = toolOutcome(block(), "notRun", locale(), timeZone);
|
|
500
552
|
return notRun
|
|
501
553
|
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
502
554
|
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
503
555
|
};
|
|
504
556
|
const declined = () => props.state === "rejected" || props.state === "not_run";
|
|
557
|
+
const label = () => {
|
|
558
|
+
const at = block().at;
|
|
559
|
+
return at && props.state !== "running" ? `${outcomeLabel()} · ${dates.formatTime(at, { locale: locale(), timeZone })}` : outcomeLabel();
|
|
560
|
+
};
|
|
505
561
|
return (
|
|
506
562
|
<Chat.Activity
|
|
507
563
|
class="ai-turn-receipt"
|
|
@@ -517,6 +573,7 @@ function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: bool
|
|
|
517
573
|
|
|
518
574
|
function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; turnId: string; phase: AiTurnPhase }) {
|
|
519
575
|
const locale = useLocale();
|
|
576
|
+
const timeZone = useReaderTimeZone();
|
|
520
577
|
let row!: HTMLDivElement;
|
|
521
578
|
// Set while focus is in this place, including after the focused control disappeared with the decided card.
|
|
522
579
|
let focusedHere = false;
|
|
@@ -547,7 +604,10 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
547
604
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
548
605
|
announce(
|
|
549
606
|
aiChatMessages(locale()).approvalNeeded({
|
|
550
|
-
title:
|
|
607
|
+
title:
|
|
608
|
+
toolSubject(current.block, locale(), timeZone) ??
|
|
609
|
+
current.block.presentation?.title ??
|
|
610
|
+
displayToolName(current.block.name, locale()),
|
|
551
611
|
}),
|
|
552
612
|
);
|
|
553
613
|
}),
|
|
@@ -641,14 +701,18 @@ function AiTurnErrorNotice(props: { error: AiTurnError; continuable: boolean })
|
|
|
641
701
|
}
|
|
642
702
|
|
|
643
703
|
/**
|
|
644
|
-
* Place 3. Live it is a status that switches in this same element; it keeps its height until the turn ends.
|
|
645
|
-
*
|
|
704
|
+
* Place 3. Live it is a status that switches in this same element; it keeps its height until the turn ends. Once a step
|
|
705
|
+
* started after it, it reads quietly like the texts in the expanded work; a trailing text may still be the answer and
|
|
706
|
+
* keeps answer typography. A status is not read token by token: screen readers hear each status once it is complete,
|
|
707
|
+
* and that the answer is ready.
|
|
646
708
|
*/
|
|
647
709
|
function AiTurnText(props: {
|
|
648
710
|
blocks: Accessor<Extract<AiTurnBlock, { kind: "text" }>[]>;
|
|
649
711
|
live: Accessor<boolean>;
|
|
650
712
|
/** The turn has work steps, so its live text is a status rather than the answer itself. */
|
|
651
713
|
folding: Accessor<boolean>;
|
|
714
|
+
/** A step started after the text, so it can no longer be the answer: it reads quietly. */
|
|
715
|
+
quiet: Accessor<boolean>;
|
|
652
716
|
/** An earlier segment of a split turn: it stops being live while the turn goes on. */
|
|
653
717
|
earlier: Accessor<boolean>;
|
|
654
718
|
}) {
|
|
@@ -662,6 +726,7 @@ function AiTurnText(props: {
|
|
|
662
726
|
);
|
|
663
727
|
const html = createMemo(() => markdown.renderSync(source(), { locale: locale() }));
|
|
664
728
|
const shownId = createMemo(() => props.blocks()[0]?.id);
|
|
729
|
+
const quiet = createMemo(() => props.quiet());
|
|
665
730
|
const [minHeight, setMinHeight] = createSignal<number | null>(null);
|
|
666
731
|
let element!: HTMLDivElement;
|
|
667
732
|
let lastHeight = 0;
|
|
@@ -673,14 +738,21 @@ function AiTurnText(props: {
|
|
|
673
738
|
observer.observe(element);
|
|
674
739
|
onCleanup(() => observer.disconnect());
|
|
675
740
|
});
|
|
741
|
+
/** The quiet text's height in answer typography, measured in place before the browser paints. */
|
|
742
|
+
const answerHeight = () => {
|
|
743
|
+
element.classList.remove("ai-turn__text--status");
|
|
744
|
+
const height = element.offsetHeight;
|
|
745
|
+
element.classList.add("ai-turn__text--status");
|
|
746
|
+
return height;
|
|
747
|
+
};
|
|
748
|
+
// A new status keeps the height of the one before it. A quiet status keeps the height it has as the answer, also when
|
|
749
|
+
// it first shows quietly after a reload, so nothing below moves when it is replaced or becomes the answer again.
|
|
676
750
|
createEffect(
|
|
677
|
-
on(
|
|
678
|
-
|
|
679
|
-
()
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
{ defer: true },
|
|
683
|
-
),
|
|
751
|
+
on([shownId, quiet], ([id, isQuiet], previous) => {
|
|
752
|
+
if (!props.live()) return;
|
|
753
|
+
const height = Math.max(previous && previous[0] !== id ? lastHeight : 0, isQuiet ? answerHeight() : 0);
|
|
754
|
+
if (height > 0) setMinHeight((current) => Math.max(current ?? 0, height));
|
|
755
|
+
}),
|
|
684
756
|
);
|
|
685
757
|
createEffect(() => {
|
|
686
758
|
if (!props.live()) setMinHeight(null);
|
|
@@ -701,6 +773,7 @@ function AiTurnText(props: {
|
|
|
701
773
|
<div
|
|
702
774
|
ref={element}
|
|
703
775
|
class="ai-turn__text"
|
|
776
|
+
classList={{ "ai-turn__text--status": quiet() }}
|
|
704
777
|
aria-live={status() ? "off" : undefined}
|
|
705
778
|
style={minHeight() ? { "min-height": `${minHeight()}px` } : undefined}
|
|
706
779
|
>
|
|
@@ -757,7 +830,13 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
|
|
|
757
830
|
</Show>
|
|
758
831
|
<For each={resultIds()}>{(id) => <AiTurnResultView result={() => results().get(id)} receipt={() => actions().has(id)} />}</For>
|
|
759
832
|
<Show when={layout().text.length > 0}>
|
|
760
|
-
<AiTurnText
|
|
833
|
+
<AiTurnText
|
|
834
|
+
blocks={() => layout().text}
|
|
835
|
+
live={live}
|
|
836
|
+
folding={() => layout().steps > 0}
|
|
837
|
+
quiet={() => layout().textIsStatus}
|
|
838
|
+
earlier={() => props.segment().earlier}
|
|
839
|
+
/>
|
|
761
840
|
</Show>
|
|
762
841
|
<For each={actionIds()}>
|
|
763
842
|
{(id) => <AiTurnActionView action={() => actions().get(id)} turnId={props.segment().turnId} phase={props.segment().phase} />}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createSignal, For } from "solid-js";
|
|
2
|
+
import { render } from "solid-js/web";
|
|
3
|
+
import type { CloudResourceRef } from "../../contracts/capabilities";
|
|
4
|
+
import type { AiTurnBlock } from "../protocol";
|
|
5
|
+
import { PresentToolBlock } from "./file-tools";
|
|
6
|
+
import { AiChatActionsProvider } from "./presentation";
|
|
7
|
+
import { CloudCardBlock } from "./visual-tools";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Presented files for a browser test, in a chat-wide column. `window.show(blocks)` replaces the shown `present`
|
|
11
|
+
* blocks and `window.showCards(args)` the shown `card` calls. Chat files load from `/files?path=`; `window.opened`
|
|
12
|
+
* records the references a reader opened.
|
|
13
|
+
*/
|
|
14
|
+
const [blocks, setBlocks] = createSignal<Extract<AiTurnBlock, { kind: "tool" }>[]>([]);
|
|
15
|
+
const [cards, setCards] = createSignal<unknown[]>([]);
|
|
16
|
+
const opened: { title: string; ref: CloudResourceRef }[] = [];
|
|
17
|
+
Object.assign(window, { show: setBlocks, showCards: setCards, opened });
|
|
18
|
+
|
|
19
|
+
const root = document.createElement("main");
|
|
20
|
+
root.style.cssText = "display: flex; flex-direction: column; gap: 1.5rem; max-width: 48rem; margin: 0 auto; padding: 1rem;";
|
|
21
|
+
document.body.append(root);
|
|
22
|
+
render(
|
|
23
|
+
() => (
|
|
24
|
+
<AiChatActionsProvider
|
|
25
|
+
actions={{
|
|
26
|
+
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
27
|
+
onOpenReference: (title, ref) => opened.push({ title, ref }),
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<For each={blocks()}>{(block) => <PresentToolBlock block={block} />}</For>
|
|
31
|
+
<For each={cards()}>{(args) => <CloudCardBlock args={args} />}</For>
|
|
32
|
+
</AiChatActionsProvider>
|
|
33
|
+
),
|
|
34
|
+
root,
|
|
35
|
+
);
|