@k2b/cloud 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/browser-code-contracts.ts +14 -2
- package/src/ai/browser.ts +8 -1
- package/src/ai/capabilities.ts +82 -22
- package/src/ai/chat/builtin-tools.tsx +31 -23
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +11 -4
- package/src/ai/chat/messages.ts +8 -2
- package/src/ai/chat/presentation.tsx +29 -9
- package/src/ai/chat/turn-view.tsx +47 -12
- package/src/ai/client/controller.ts +60 -31
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +7 -2
- package/src/ai/code-mode-skill.ts +27 -27
- package/src/ai/code-runtime-tools.ts +10 -1
- package/src/ai/code-source-contracts.ts +54 -4
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +2 -2
- package/src/ai/default-tools.ts +2 -2
- package/src/ai/executor.ts +148 -84
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +7 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/protocol.ts +6 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +2 -2
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +34 -6
- package/src/ai/runtime.ts +1 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +31 -3
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +109 -57
- package/src/ai/stream.ts +180 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +25 -0
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-policy.ts +247 -0
- package/src/ai/types.ts +24 -3
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +62 -0
- package/src/api/index.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +118 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +77 -0
- package/src/contracts/registry.ts +2 -0
- package/src/services/index.ts +3 -0
- package/src/services/notifications/email.ts +16 -26
- package/src/services/outgoing-mail/index.ts +19 -0
- package/src/services/outgoing-mail/store.ts +286 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +13 -0
- package/src/services/settings/core-settings.ts +1 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-request-options.ts +185 -0
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/workspace-navigation.ts +7 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
untrack,
|
|
16
16
|
} from "solid-js";
|
|
17
17
|
import { markdown } from "../../shared";
|
|
18
|
-
import type
|
|
18
|
+
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
19
|
import { ApprovalBlockView, CompactionBlockView, CompactToolRow, SurveyToolView, TextEditorToolView } from "./blocks";
|
|
20
20
|
import { CapabilityTablePreview } from "./capability-table";
|
|
21
21
|
import { PresentToolBlock } from "./file-tools";
|
|
@@ -47,8 +47,12 @@ export type AiTurnSegment = {
|
|
|
47
47
|
duration: () => AiTurnDuration | null;
|
|
48
48
|
/** Shown above the work of a delivered scheduled result. */
|
|
49
49
|
scheduledTask?: { taskId: string; occurrenceId: string } | null;
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The live turn cannot reach the model or the server right now: a model call waits for its retry, or the stream
|
|
52
|
+
* reconnects. The working segment's work line says so while its clock stands; a turn without a work line yet says
|
|
53
|
+
* it in a row at the end of its last segment.
|
|
54
|
+
*/
|
|
55
|
+
reconnect?: "line" | "row";
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
const isLive = (phase: AiTurnPhase) => phase === "running" || phase === "waiting";
|
|
@@ -63,6 +67,10 @@ export const formatWorkDuration = (ms: number, t: Messages, long = false): strin
|
|
|
63
67
|
return long ? t.durationHoursLong({ hours, minutes: minutes % 60 }) : t.durationHours({ hours, minutes: minutes % 60 });
|
|
64
68
|
};
|
|
65
69
|
|
|
70
|
+
/** Identity of the current step: it changes when another block becomes current or the current call changes state. */
|
|
71
|
+
const stepKey = (block: AiTurnBlock | null): string =>
|
|
72
|
+
!block ? "start" : `${block.id}:${block.kind === "tool" || block.kind === "compaction" ? block.status : block.kind}`;
|
|
73
|
+
|
|
66
74
|
/** A running clock: "2:41", "1:02:41". */
|
|
67
75
|
export const formatWorkClock = (ms: number): string => {
|
|
68
76
|
const total = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -80,14 +88,25 @@ const toolTarget = (block: ToolBlock): string => {
|
|
|
80
88
|
return typeof value === "string" ? basename(value.trim()) : "";
|
|
81
89
|
};
|
|
82
90
|
|
|
83
|
-
/**
|
|
84
|
-
export const
|
|
91
|
+
/** A step that runs longer than this shows its duration, so a long step does not look like a stalled one. */
|
|
92
|
+
export const AI_LONG_STEP_MS = AI_TURN_LEASE_MS;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The current step of a live turn, as a short progressive phrase. A step that runs long shows its duration in place of
|
|
96
|
+
* its target: "Running code · 3 min".
|
|
97
|
+
*/
|
|
98
|
+
export const liveStepLabel = (block: AiTurnBlock | null, t: Messages, stepMs = 0): string => {
|
|
99
|
+
if (stepMs < AI_LONG_STEP_MS) return stepPhrase(block, t, true);
|
|
100
|
+
return `${stepPhrase(block, t, false)} · ${formatWorkDuration(stepMs, t)}`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean): string => {
|
|
85
104
|
if (!block || block.kind === "thinking" || block.kind === "steer_applied" || block.kind === "steer_message") return t.stepThinking;
|
|
86
105
|
if (block.kind === "text") return t.stepWriting;
|
|
87
106
|
if (block.kind === "compaction") return block.status === "running" ? t.stepCompacting : t.stepThinking;
|
|
88
107
|
// A finished call means the model is deciding what to do next.
|
|
89
108
|
if (block.status !== "running" && block.status !== "awaiting_client") return t.stepThinking;
|
|
90
|
-
const target = toolTarget(block);
|
|
109
|
+
const target = withTargets ? toolTarget(block) : "";
|
|
91
110
|
const withTarget = (label: string) => (target ? `${label} · ${target}` : label);
|
|
92
111
|
const name = block.name;
|
|
93
112
|
if (block.presentation?.kind === "capability") return withTarget(t.stepUsingApp({ app: block.presentation.appName }));
|
|
@@ -117,15 +136,31 @@ function AiWorkLine(props: { segment: Accessor<AiTurnSegment> }) {
|
|
|
117
136
|
const t = () => aiChatMessages(locale());
|
|
118
137
|
const segment = () => props.segment();
|
|
119
138
|
const layout = () => segment().layout;
|
|
120
|
-
const
|
|
121
|
-
const
|
|
139
|
+
const reconnecting = () => isLive(segment().phase) && segment().reconnect === "line";
|
|
140
|
+
const waiting = () => !reconnecting() && segment().phase === "waiting" && layout().waitingFor !== null;
|
|
141
|
+
const working = () => isLive(segment().phase) && !waiting() && !reconnecting();
|
|
142
|
+
// The current step is timed on the work clock, so it stands while the turn reconnects.
|
|
143
|
+
const currentStep = createMemo(() => (isLive(segment().phase) ? stepKey(layout().current) : null));
|
|
144
|
+
const step = createMemo(on(currentStep, (key) => (key === null ? null : (untrack(segment().duration)?.workedMs ?? 0))));
|
|
145
|
+
const stepMs = () => {
|
|
146
|
+
const startedAt = step();
|
|
147
|
+
const workedMs = segment().duration()?.workedMs;
|
|
148
|
+
return startedAt === null || workedMs === undefined ? 0 : workedMs - startedAt;
|
|
149
|
+
};
|
|
150
|
+
// Screen readers hear once that the turn reconnects; the line itself is not read while it changes.
|
|
151
|
+
createEffect(
|
|
152
|
+
on(reconnecting, (now, before) => {
|
|
153
|
+
if (now && !before) announce(t().reconnecting);
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
122
156
|
const worked = () => {
|
|
123
157
|
const duration = segment().duration();
|
|
124
158
|
return !segment().earlier && duration && duration.workedMs > 0 ? duration.workedMs : null;
|
|
125
159
|
};
|
|
126
160
|
const steps = () => t().steps({ count: layout().steps });
|
|
127
161
|
const label = () => {
|
|
128
|
-
if (
|
|
162
|
+
if (reconnecting()) return t().reconnecting;
|
|
163
|
+
if (working()) return liveStepLabel(layout().current, t(), stepMs());
|
|
129
164
|
if (waiting()) return layout().waitingFor === "approval" ? t().waitingForApproval : t().waitingForAnswer;
|
|
130
165
|
const duration = worked();
|
|
131
166
|
const short = duration === null ? "" : formatWorkDuration(duration, t());
|
|
@@ -145,7 +180,7 @@ function AiWorkLine(props: { segment: Accessor<AiTurnSegment> }) {
|
|
|
145
180
|
};
|
|
146
181
|
const trailing = () => {
|
|
147
182
|
const duration = segment().duration();
|
|
148
|
-
if (working() && duration) {
|
|
183
|
+
if ((working() || reconnecting()) && duration) {
|
|
149
184
|
return (
|
|
150
185
|
<span class="ai-turn-work__meta">
|
|
151
186
|
{formatWorkClock(duration.workedMs)}
|
|
@@ -599,8 +634,8 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
|
|
|
599
634
|
<For each={actionIds()}>
|
|
600
635
|
{(id) => <AiTurnActionView action={() => actions().get(id)} turnId={props.segment().turnId} phase={props.segment().phase} />}
|
|
601
636
|
</For>
|
|
602
|
-
<Show when={props.segment().
|
|
603
|
-
{/* Calm by design: no busy sweep while the
|
|
637
|
+
<Show when={props.segment().reconnect === "row"}>
|
|
638
|
+
{/* Calm by design: no busy sweep while the turn reconnects. */}
|
|
604
639
|
<Chat.Activity label={aiChatMessages(locale()).reconnecting} icon="ti ti-refresh" />
|
|
605
640
|
</Show>
|
|
606
641
|
</div>
|
|
@@ -2,7 +2,7 @@ import { type Accessor, createMemo, createSignal, onCleanup } from "solid-js";
|
|
|
2
2
|
import { createStore, reconcile } from "solid-js/store";
|
|
3
3
|
import { type AiAttachmentRef, aiAttachmentMarker } from "../attachments";
|
|
4
4
|
import { aiChatMessages } from "../chat/messages";
|
|
5
|
-
import { type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
|
|
5
|
+
import { AI_TURN_LEASE_MS, type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
|
|
6
6
|
import { type AiResourceMarker, aiResourceMarker } from "../resource-markers";
|
|
7
7
|
import type {
|
|
8
8
|
AiClientToolId,
|
|
@@ -201,6 +201,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
201
201
|
const abortRequests = new Map<string, Promise<boolean>>();
|
|
202
202
|
let stream: AiStreamHandle | null = null;
|
|
203
203
|
let streamSession: AiStreamSession | null = null;
|
|
204
|
+
let silenceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
204
205
|
/** The chat error left by a stream that ended for good, until a stream opens again. */
|
|
205
206
|
let streamError: string | null = null;
|
|
206
207
|
let streamGeneration = 0;
|
|
@@ -294,8 +295,32 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
294
295
|
void markConversationViewed(session.conversationId);
|
|
295
296
|
};
|
|
296
297
|
|
|
298
|
+
const clearSilenceTimer = () => {
|
|
299
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
300
|
+
silenceTimer = undefined;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A running turn whose open stream delivers nothing for one worker lease either works silently or lost an update,
|
|
305
|
+
* such as its end. The stream then starts again from the turn's saved state. The wait starts with the stream's
|
|
306
|
+
* first event, so a slow first state is never cut off. A turn that waits for an action in this browser is not
|
|
307
|
+
* silent, and a stream that reconnects brings a fresh state anyway.
|
|
308
|
+
*/
|
|
309
|
+
const watchSilence = (session: AiStreamSession) => {
|
|
310
|
+
clearSilenceTimer();
|
|
311
|
+
silenceTimer = setTimeout(() => {
|
|
312
|
+
silenceTimer = undefined;
|
|
313
|
+
if (!isCurrentStreamSession(streamSession, session)) return;
|
|
314
|
+
const status = streamStatus();
|
|
315
|
+
if (state.activeTurn?.status !== "running" || status === "connecting" || status === "reconnecting") return watchSilence(session);
|
|
316
|
+
closeStream();
|
|
317
|
+
openStream(session.conversationId);
|
|
318
|
+
}, AI_TURN_LEASE_MS);
|
|
319
|
+
};
|
|
320
|
+
|
|
297
321
|
const applyEvent = (session: AiStreamSession, event: AiStreamEvent) => {
|
|
298
322
|
if (!isCurrentStreamSession(streamSession, session)) return;
|
|
323
|
+
watchSilence(session);
|
|
299
324
|
const conversationId = session.conversationId;
|
|
300
325
|
if (event.type === "turn_finished" && pendingSends()[conversationId]) {
|
|
301
326
|
completedDuringSend.set(conversationId, event.turnId);
|
|
@@ -327,6 +352,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
327
352
|
};
|
|
328
353
|
|
|
329
354
|
const closeStream = () => {
|
|
355
|
+
clearSilenceTimer();
|
|
330
356
|
stream?.close();
|
|
331
357
|
stream = null;
|
|
332
358
|
streamSession = null;
|
|
@@ -400,15 +426,15 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
400
426
|
console.warn(`No frontend handler registered for AI tool "${name}" — leaving the action request pending.`);
|
|
401
427
|
return false;
|
|
402
428
|
}
|
|
429
|
+
let result: unknown;
|
|
403
430
|
try {
|
|
404
|
-
|
|
405
|
-
return submitTurnActionForConversation(conversationId, turnId, callId, { type: "tool_result", result });
|
|
431
|
+
result = await handler({ name, callId, args, turnId, conversationId });
|
|
406
432
|
} catch (toolError) {
|
|
407
|
-
|
|
408
|
-
type: "tool_result",
|
|
409
|
-
result: { error: toolError instanceof Error ? toolError.message : "Frontend tool failed" },
|
|
410
|
-
});
|
|
433
|
+
result = { error: toolError instanceof Error ? toolError.message : "Frontend tool failed" };
|
|
411
434
|
}
|
|
435
|
+
const submitted = await submitTurnActionForConversation(conversationId, turnId, callId, { type: "tool_result", result });
|
|
436
|
+
if (submitted) showActionResolved(conversationId, turnId, { type: "tool_result", callId, result });
|
|
437
|
+
return submitted;
|
|
412
438
|
};
|
|
413
439
|
|
|
414
440
|
// ---- conversation loading --------------------------------------------
|
|
@@ -1258,6 +1284,28 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1258
1284
|
}
|
|
1259
1285
|
};
|
|
1260
1286
|
|
|
1287
|
+
/**
|
|
1288
|
+
* Shows an action the server accepted as resolved, so the turn runs on before the stream confirms it. A turn that
|
|
1289
|
+
* runs is watched for silence again, which heals a confirmation that never arrives.
|
|
1290
|
+
*/
|
|
1291
|
+
const showActionResolved = (
|
|
1292
|
+
conversationId: string,
|
|
1293
|
+
turnId: string,
|
|
1294
|
+
action: { type: "approval_response"; callId: string; approved: boolean } | { type: "tool_result"; callId: string; result: unknown },
|
|
1295
|
+
) => {
|
|
1296
|
+
if (!isActiveConversation(conversationId) || state.activeTurn?.turnId !== turnId) return;
|
|
1297
|
+
const waitingStatus = action.type === "approval_response" ? "awaiting_approval" : "awaiting_client";
|
|
1298
|
+
setState("activeTurn", (current) => {
|
|
1299
|
+
if (!current || current.turnId !== turnId) return current;
|
|
1300
|
+
const pending = current.blocks.some(
|
|
1301
|
+
(block) => block.kind === "tool" && block.callId === action.callId && block.status === waitingStatus,
|
|
1302
|
+
);
|
|
1303
|
+
if (!pending) return current;
|
|
1304
|
+
return reconcileActiveTurnActions(current, [{ callId: action.callId, resolvedEvent: action }]);
|
|
1305
|
+
});
|
|
1306
|
+
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1261
1309
|
const submitTurnAction = (
|
|
1262
1310
|
turnId: string,
|
|
1263
1311
|
callId: string,
|
|
@@ -1277,18 +1325,9 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1277
1325
|
approved: input.approved,
|
|
1278
1326
|
remember: input.remember,
|
|
1279
1327
|
});
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if (!current || current.turnId !== request.turnId) return current;
|
|
1284
|
-
const pending = current.blocks.some(
|
|
1285
|
-
(block) => block.kind === "tool" && block.callId === request.callId && block.status === "awaiting_approval",
|
|
1286
|
-
);
|
|
1287
|
-
if (!pending) return current;
|
|
1288
|
-
return reconcileActiveTurnActions(current, [{ callId: request.callId, resolvedEvent: action }]);
|
|
1289
|
-
});
|
|
1290
|
-
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1291
|
-
return true;
|
|
1328
|
+
if (submitted)
|
|
1329
|
+
showActionResolved(conversationId, request.turnId, { type: "approval_response", callId: request.callId, approved: input.approved });
|
|
1330
|
+
return submitted;
|
|
1292
1331
|
};
|
|
1293
1332
|
|
|
1294
1333
|
const submitFrontendToolResult = async (request: { turnId: string; callId: string }, result: unknown) => {
|
|
@@ -1299,18 +1338,8 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1299
1338
|
type: "tool_result",
|
|
1300
1339
|
result,
|
|
1301
1340
|
});
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1304
|
-
setState("activeTurn", (current) => {
|
|
1305
|
-
if (!current || current.turnId !== request.turnId) return current;
|
|
1306
|
-
const pending = current.blocks.some(
|
|
1307
|
-
(block) => block.kind === "tool" && block.callId === request.callId && block.status === "awaiting_client",
|
|
1308
|
-
);
|
|
1309
|
-
if (!pending) return current;
|
|
1310
|
-
return reconcileActiveTurnActions(current, [{ callId: request.callId, resolvedEvent: action }]);
|
|
1311
|
-
});
|
|
1312
|
-
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1313
|
-
return true;
|
|
1341
|
+
if (submitted) showActionResolved(conversationId, request.turnId, { type: "tool_result", callId: request.callId, result });
|
|
1342
|
+
return submitted;
|
|
1314
1343
|
};
|
|
1315
1344
|
|
|
1316
1345
|
if (options.initialConversationId) {
|
|
@@ -30,7 +30,14 @@ const bytesToBase64 = (bytes: Uint8Array): string => {
|
|
|
30
30
|
return btoa(binary);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export
|
|
33
|
+
export type ConversationFileSource = FileSource & {
|
|
34
|
+
/** Files below `prefix` (default `/`); with `page`, at most `page.limit` files by path after `page.after`. */
|
|
35
|
+
listFiles(prefix?: string, page?: { limit: number; after?: string }): Promise<AiFileStat[]>;
|
|
36
|
+
/** Delete a folder with everything below it. */
|
|
37
|
+
removeFolder(path: string): Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const conversationFileSource = (baseUrl: string, conversationId: string): ConversationFileSource => {
|
|
34
41
|
const filesUrl = (suffix = "", params?: Record<string, string>) => {
|
|
35
42
|
const query = params ? `?${new URLSearchParams(params).toString()}` : "";
|
|
36
43
|
return `${baseUrl}/conversations/${conversationId}/files${suffix}${query}`;
|
|
@@ -45,8 +52,14 @@ export const conversationFileSource = (baseUrl: string, conversationId: string):
|
|
|
45
52
|
return (await response.json()) as T;
|
|
46
53
|
};
|
|
47
54
|
|
|
48
|
-
const listFiles = async (): Promise<AiFileStat[]> =>
|
|
49
|
-
|
|
55
|
+
const listFiles = async (prefix?: string, page?: { limit: number; after?: string }): Promise<AiFileStat[]> => {
|
|
56
|
+
const params: Record<string, string> = {
|
|
57
|
+
...(prefix ? { prefix } : {}),
|
|
58
|
+
...(page ? { limit: String(page.limit), ...(page.after ? { after: page.after } : {}) } : {}),
|
|
59
|
+
};
|
|
60
|
+
const url = Object.keys(params).length ? filesUrl("", params) : filesUrl();
|
|
61
|
+
return (await request<{ files: AiFileStat[] }>(url, { method: "GET" }, "Failed to load files")).files;
|
|
62
|
+
};
|
|
50
63
|
|
|
51
64
|
return {
|
|
52
65
|
previewPreferencesKey: "assistant.csv-preview",
|
|
@@ -80,6 +93,10 @@ export const conversationFileSource = (baseUrl: string, conversationId: string):
|
|
|
80
93
|
await request(filesUrl("", { path }), { method: "DELETE" }, "Failed to delete file");
|
|
81
94
|
},
|
|
82
95
|
|
|
96
|
+
async removeFolder(path) {
|
|
97
|
+
await request(filesUrl("", { path, recursive: "true" }), { method: "DELETE" }, "Failed to delete folder");
|
|
98
|
+
},
|
|
99
|
+
|
|
83
100
|
async rename(from, to) {
|
|
84
101
|
await request(filesUrl("/rename"), { method: "POST", body: JSON.stringify({ from, to }) }, "Failed to rename file");
|
|
85
102
|
},
|
|
@@ -9,7 +9,10 @@ export type AiActiveTurn = {
|
|
|
9
9
|
status: "running" | "waiting_for_action";
|
|
10
10
|
blocks: AiTurnBlock[];
|
|
11
11
|
modelProfileId: string | null;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* A model call failed transiently and waits for its retry. Live only: the next turn event ends it, and so does a
|
|
14
|
+
* snapshot that has moved past the retry.
|
|
15
|
+
*/
|
|
13
16
|
providerRetry?: boolean;
|
|
14
17
|
/** Server start of the turn, known from a state snapshot. */
|
|
15
18
|
createdAt?: string;
|
|
@@ -98,7 +101,9 @@ export const mergeActiveTurn = (previous: AiActiveTurn | null, incoming: AiActiv
|
|
|
98
101
|
const pendingSteers = previous.blocks.filter((block) => block.kind === "steer_message" && block.status !== "consumed");
|
|
99
102
|
const known = new Set(incoming.blocks.map((block) => block.id));
|
|
100
103
|
const blocks = [...incoming.blocks, ...pendingSteers.filter((block) => !known.has(block.id))];
|
|
101
|
-
|
|
104
|
+
// A snapshot never carries a provider retry. One at the retry's own position has seen nothing after it, so the wait goes on.
|
|
105
|
+
const retry = previous.providerRetry && incoming.seq === previous.seq ? { providerRetry: true } : {};
|
|
106
|
+
return { ...turnTiming(previous), ...incoming, ...retry, blocks, status: deriveStatus(blocks) };
|
|
102
107
|
};
|
|
103
108
|
|
|
104
109
|
export const reconcileActiveTurnActions = (
|