@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { announce, Button, ButtonLink, Chat, NoticeCard, useLocale } from "@k2b/ui";
|
|
1
|
+
import { announce, Button, Chat, NoticeCard, useLocale } from "@k2b/ui";
|
|
3
2
|
import {
|
|
4
3
|
type Accessor,
|
|
5
4
|
createEffect,
|
|
@@ -16,36 +15,23 @@ import {
|
|
|
16
15
|
untrack,
|
|
17
16
|
} from "solid-js";
|
|
18
17
|
import { markdown } from "../../shared";
|
|
18
|
+
import { type ChatTaskReason, chatTaskReasonSentence, chatTaskReasonState, chatTaskStopSentence } from "../chat-task-reasons";
|
|
19
19
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
20
20
|
import type { AiTurnError } from "../types";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
type AiApprovalDecision,
|
|
24
|
-
ApprovalBlockView,
|
|
25
|
-
CompactionBlockView,
|
|
26
|
-
CompactToolRow,
|
|
27
|
-
SurveyToolView,
|
|
28
|
-
TextEditorToolView,
|
|
29
|
-
} from "./blocks";
|
|
21
|
+
import { toolSubject, useReaderTimeZone } from "./action-sentences";
|
|
22
|
+
import { CompactionBlockView, CompactToolRow, SurveyToolView, TextEditorToolView } from "./blocks";
|
|
30
23
|
import { CapabilityTablePreview } from "./capability-table";
|
|
31
24
|
import { CloudChartBlock } from "./chart-block";
|
|
32
25
|
import { PresentToolBlock } from "./file-tools";
|
|
33
26
|
import { useAiChatActions } from "./message-actions";
|
|
34
|
-
import {
|
|
35
|
-
aiToolIcon,
|
|
36
|
-
capabilityErrorDescription,
|
|
37
|
-
displayToolName,
|
|
38
|
-
isCardToolName,
|
|
39
|
-
isChartToolName,
|
|
40
|
-
isRecord,
|
|
41
|
-
isSurveyToolName,
|
|
42
|
-
} from "./message-utils";
|
|
27
|
+
import { aiToolIcon, displayToolName, isCardToolName, isChartToolName, isRecord, isSurveyToolName } from "./message-utils";
|
|
43
28
|
import { aiChatMessages } from "./messages";
|
|
44
29
|
import { AssistantMarkdownBlock } from "./primitives";
|
|
30
|
+
import { ResultLinks, resultLinks, ToolActionView } from "./tool-action";
|
|
45
31
|
import { AiToolActivity, AiToolDisclosureProvider, type AiToolDisclosureState } from "./tool-disclosure";
|
|
46
32
|
import { type AiWorkEntry, type AiWorkItem, countWorkSteps, groupWorkBlocks } from "./tool-groups";
|
|
47
33
|
import { aiTurnErrorCanContinue, aiTurnErrorDescription } from "./turn-error";
|
|
48
|
-
import type { AiTurnAction,
|
|
34
|
+
import type { AiTurnAction, AiTurnLayout, AiTurnPhase, AiTurnResult } from "./turn-layout";
|
|
49
35
|
import { CloudCardBlock } from "./visual-tools";
|
|
50
36
|
|
|
51
37
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -65,8 +51,17 @@ export type AiTurnSegment = {
|
|
|
65
51
|
earlier: boolean;
|
|
66
52
|
/** Read reactively, so a ticking clock does not rebuild the segment. */
|
|
67
53
|
duration: () => AiTurnDuration | null;
|
|
68
|
-
/**
|
|
69
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Shown above the work of a delivered scheduled result. `stop` and `reason` add a system line the chat words in
|
|
56
|
+
* the reader's language; with `stop` or `reasonOnly` that line replaces the stored text.
|
|
57
|
+
*/
|
|
58
|
+
scheduledTask?: {
|
|
59
|
+
taskId: string;
|
|
60
|
+
occurrenceId: string;
|
|
61
|
+
reason?: ChatTaskReason;
|
|
62
|
+
reasonOnly?: true;
|
|
63
|
+
stop?: { cause: "paused" | "deleted"; done: string[] };
|
|
64
|
+
} | null;
|
|
70
65
|
/**
|
|
71
66
|
* The live turn cannot reach the model or the server right now: a model call waits for its retry, or the stream
|
|
72
67
|
* reconnects. The working segment's work line says so while its clock stands; a turn without a work line yet says
|
|
@@ -417,42 +412,6 @@ const resultSummary = (block: ToolBlock): string => {
|
|
|
417
412
|
return typeof value === "string" && value.trim().length <= 500 ? value.trim() : "";
|
|
418
413
|
};
|
|
419
414
|
|
|
420
|
-
/** Links a result offers, limited to paths inside this Cloud. */
|
|
421
|
-
const resultLinks = (block: ToolBlock): Record<string, unknown>[] => {
|
|
422
|
-
const value = resultRecord(block)?.links;
|
|
423
|
-
return Array.isArray(value)
|
|
424
|
-
? value.filter(
|
|
425
|
-
(link): link is Record<string, unknown> =>
|
|
426
|
-
isRecord(link) && typeof link.href === "string" && /^\/(?![\\/])[^\\\u0000-\u001f\u007f]*$/.test(link.href),
|
|
427
|
-
)
|
|
428
|
-
: [];
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
function ResultLinks(props: { links: Record<string, unknown>[] }) {
|
|
432
|
-
const locale = useLocale();
|
|
433
|
-
const t = () => aiChatMessages(locale());
|
|
434
|
-
const label = (link: Record<string, unknown>) =>
|
|
435
|
-
typeof link.title === "string" ? link.title : link.rel === "edit" ? t().edit : link.rel === "download" ? t().download : t().open;
|
|
436
|
-
return (
|
|
437
|
-
<span class="flex min-w-0 flex-wrap items-center justify-end gap-1">
|
|
438
|
-
<For each={props.links}>
|
|
439
|
-
{(link) => (
|
|
440
|
-
<ButtonLink
|
|
441
|
-
class="ai-chat-result-link"
|
|
442
|
-
href={String(link.href)}
|
|
443
|
-
target="_blank"
|
|
444
|
-
rel="noopener noreferrer"
|
|
445
|
-
size="xs"
|
|
446
|
-
variant="ghost"
|
|
447
|
-
>
|
|
448
|
-
{label(link)}
|
|
449
|
-
</ButtonLink>
|
|
450
|
-
)}
|
|
451
|
-
</For>
|
|
452
|
-
</span>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
415
|
/** The summary and links of a table result, unless its receipt in place 4 already shows them. */
|
|
457
416
|
function CapabilityResultRow(props: { block: ToolBlock }) {
|
|
458
417
|
const locale = useLocale();
|
|
@@ -516,87 +475,11 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
516
475
|
);
|
|
517
476
|
}
|
|
518
477
|
|
|
519
|
-
type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* One line for an action in place 4, the same element while it runs and once it is done, rejected, failed, or not run.
|
|
523
|
-
* It says what happened in the owning app's words: its sentence for the outcome, such as "Moved report.pdf to the
|
|
524
|
-
* trash", else, once done, the application's own summary of the call. Without either it puts the state in the reader's
|
|
525
|
-
* language before what the call does, such as "Rejected: Write report.md", or before its title and target.
|
|
526
|
-
*/
|
|
527
|
-
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
528
|
-
const locale = useLocale();
|
|
529
|
-
const timeZone = useReaderTimeZone();
|
|
530
|
-
const t = () => aiChatMessages(locale());
|
|
531
|
-
const block = () => props.block;
|
|
532
|
-
const subject = () => {
|
|
533
|
-
const sentence = toolSubject(block(), locale(), timeZone);
|
|
534
|
-
if (sentence) return sentence;
|
|
535
|
-
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
536
|
-
const target = toolTarget(block());
|
|
537
|
-
return target ? `${title} · ${target}` : title;
|
|
538
|
-
};
|
|
539
|
-
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
540
|
-
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
541
|
-
const outcomeLabel = () => {
|
|
542
|
-
const state = props.state;
|
|
543
|
-
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
544
|
-
if (state === "done") {
|
|
545
|
-
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
546
|
-
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
547
|
-
return toolOutcome(block(), "done", locale(), timeZone) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
548
|
-
}
|
|
549
|
-
if (state === "rejected") return toolOutcome(block(), "rejected", locale(), timeZone) ?? t().receiptRejected({ title: subject() });
|
|
550
|
-
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
551
|
-
const notRun = toolOutcome(block(), "notRun", locale(), timeZone);
|
|
552
|
-
return notRun
|
|
553
|
-
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
554
|
-
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
555
|
-
};
|
|
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
|
-
};
|
|
561
|
-
return (
|
|
562
|
-
<Chat.Activity
|
|
563
|
-
class="ai-turn-receipt"
|
|
564
|
-
icon={declined() ? "ti ti-circle-off" : aiToolIcon(block().name, block().presentation?.appIcon)}
|
|
565
|
-
accent={declined() ? undefined : block().presentation?.appAccent}
|
|
566
|
-
label={label()}
|
|
567
|
-
busy={props.state === "running"}
|
|
568
|
-
description={props.state === "failed" ? capabilityErrorDescription(block().result) : undefined}
|
|
569
|
-
trailing={links().length > 0 ? <ResultLinks links={links()} /> : undefined}
|
|
570
|
-
/>
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
478
|
function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; turnId: string; phase: AiTurnPhase }) {
|
|
575
479
|
const locale = useLocale();
|
|
576
480
|
const timeZone = useReaderTimeZone();
|
|
577
|
-
let row!: HTMLDivElement;
|
|
578
|
-
// Set while focus is in this place, including after the focused control disappeared with the decided card.
|
|
579
|
-
let focusedHere = false;
|
|
580
|
-
// The decision the server accepted for a card here; its receipt replaces the card before the turn reports it. It
|
|
581
|
-
// belongs to the call it answered: code that asks again shows the next request on the same block as a new card.
|
|
582
|
-
const [decided, setDecided] = createSignal<AiApprovalDecision | null>(null);
|
|
583
481
|
const action = () => props.action();
|
|
584
482
|
const state = () => action()?.state;
|
|
585
|
-
const decision = () => {
|
|
586
|
-
const current = action();
|
|
587
|
-
const value = decided();
|
|
588
|
-
return current && value?.callId === current.block.callId ? value.approved : null;
|
|
589
|
-
};
|
|
590
|
-
const card = () => {
|
|
591
|
-
const current = action();
|
|
592
|
-
return current?.state === "open" && current.block.status === "awaiting_approval" && decision() === null ? current.block : undefined;
|
|
593
|
-
};
|
|
594
|
-
const receipt = (): ReceiptState | undefined => {
|
|
595
|
-
const current = action();
|
|
596
|
-
if (!current || card()) return undefined;
|
|
597
|
-
if (current.state === "open" && current.block.status === "awaiting_approval") return decision() ? "running" : "rejected";
|
|
598
|
-
return current.state === "open" || current.state === "interaction" ? undefined : current.state;
|
|
599
|
-
};
|
|
600
483
|
// Screen readers hear a waiting approval once, as one short line instead of the whole card, without moving focus.
|
|
601
484
|
createEffect(
|
|
602
485
|
on(state, (next, previous) => {
|
|
@@ -612,37 +495,14 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
612
495
|
);
|
|
613
496
|
}),
|
|
614
497
|
);
|
|
615
|
-
// A card the user decided becomes its receipt in place, once, as the direct result of the decision. Focus stays on
|
|
616
|
-
// that place without scrolling.
|
|
617
|
-
createEffect(
|
|
618
|
-
on(
|
|
619
|
-
() => card() !== undefined,
|
|
620
|
-
(open, wasOpen) => {
|
|
621
|
-
if (!wasOpen || open || !focusedHere || typeof document === "undefined") return;
|
|
622
|
-
const focused = document.activeElement;
|
|
623
|
-
if (!focused || focused === document.body || row.contains(focused)) row.focus({ preventScroll: true });
|
|
624
|
-
},
|
|
625
|
-
),
|
|
626
|
-
);
|
|
627
498
|
return (
|
|
628
|
-
<div
|
|
629
|
-
ref={row}
|
|
630
|
-
class="ai-turn__action"
|
|
631
|
-
aria-live="off"
|
|
632
|
-
tabIndex={-1}
|
|
633
|
-
onFocusIn={() => {
|
|
634
|
-
focusedHere = true;
|
|
635
|
-
}}
|
|
636
|
-
onFocusOut={(event) => {
|
|
637
|
-
if (event.relatedTarget instanceof Node && !row.contains(event.relatedTarget)) focusedHere = false;
|
|
638
|
-
}}
|
|
639
|
-
>
|
|
499
|
+
<div class="ai-turn__action" aria-live="off">
|
|
640
500
|
<Show when={action()}>
|
|
641
501
|
{(current) => (
|
|
642
502
|
<Switch>
|
|
643
|
-
|
|
644
|
-
<Match when={
|
|
645
|
-
|
|
503
|
+
{/* A question and every outcome of its call share one element, so an answer collapses it in place. */}
|
|
504
|
+
<Match when={current().state !== "interaction"}>
|
|
505
|
+
<ToolActionView block={current().block} turnId={props.turnId} state={current().state} phase={props.phase} />
|
|
646
506
|
</Match>
|
|
647
507
|
<Match when={true}>
|
|
648
508
|
<Switch
|
|
@@ -789,6 +649,7 @@ function AiTurnText(props: {
|
|
|
789
649
|
*/
|
|
790
650
|
export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosureState: AiToolDisclosureState }): JSX.Element {
|
|
791
651
|
const locale = useLocale();
|
|
652
|
+
const timeZone = useReaderTimeZone();
|
|
792
653
|
const chatActions = useAiChatActions();
|
|
793
654
|
const layout = () => props.segment().layout;
|
|
794
655
|
const results = createMemo(() => new Map(layout().results.map((result) => [result.id, result])));
|
|
@@ -829,7 +690,17 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
|
|
|
829
690
|
</div>
|
|
830
691
|
</Show>
|
|
831
692
|
<For each={resultIds()}>{(id) => <AiTurnResultView result={() => results().get(id)} receipt={() => actions().has(id)} />}</For>
|
|
832
|
-
<Show when={
|
|
693
|
+
<Show when={props.segment().scheduledTask?.stop}>
|
|
694
|
+
{(stop) => (
|
|
695
|
+
<Chat.Activity
|
|
696
|
+
class="ai-turn__routine-line"
|
|
697
|
+
wrap
|
|
698
|
+
icon="ti ti-player-stop"
|
|
699
|
+
label={chatTaskStopSentence(stop().done, locale())}
|
|
700
|
+
/>
|
|
701
|
+
)}
|
|
702
|
+
</Show>
|
|
703
|
+
<Show when={layout().text.length > 0 && !props.segment().scheduledTask?.stop && !props.segment().scheduledTask?.reasonOnly}>
|
|
833
704
|
<AiTurnText
|
|
834
705
|
blocks={() => layout().text}
|
|
835
706
|
live={live}
|
|
@@ -838,6 +709,17 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
|
|
|
838
709
|
earlier={() => props.segment().earlier}
|
|
839
710
|
/>
|
|
840
711
|
</Show>
|
|
712
|
+
<Show when={props.segment().scheduledTask?.stop ? undefined : props.segment().scheduledTask?.reason}>
|
|
713
|
+
{(reason) => (
|
|
714
|
+
<Chat.Activity
|
|
715
|
+
class="ai-turn__routine-line"
|
|
716
|
+
wrap
|
|
717
|
+
icon={chatTaskReasonState(reason().code) === "paused" ? "ti ti-player-pause" : "ti ti-alert-triangle"}
|
|
718
|
+
tone={chatTaskReasonState(reason().code) === "paused" ? "neutral" : "warning"}
|
|
719
|
+
label={chatTaskReasonSentence(reason(), { locale: locale(), timeZone })}
|
|
720
|
+
/>
|
|
721
|
+
)}
|
|
722
|
+
</Show>
|
|
841
723
|
<For each={actionIds()}>
|
|
842
724
|
{(id) => <AiTurnActionView action={() => actions().get(id)} turnId={props.segment().turnId} phase={props.segment().phase} />}
|
|
843
725
|
</For>
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
useLocale,
|
|
11
11
|
} from "@k2b/ui";
|
|
12
12
|
import { createMemo, createSignal, createUniqueId, Index, type JSX, Match, onCleanup, onMount, Show, Switch } from "solid-js";
|
|
13
|
-
import type
|
|
13
|
+
import { type CloudAiChartRow, cloudAiNumberFormatters } from "../chart-block";
|
|
14
14
|
import {
|
|
15
15
|
CLOUD_AI_DETAILS_VIEWPORT_ITEMS,
|
|
16
16
|
CLOUD_AI_VIEW_MAX_BYTES,
|
|
@@ -39,8 +39,11 @@ import { ReferenceText } from "./reference-text";
|
|
|
39
39
|
const VISIBLE_ROWS = 100;
|
|
40
40
|
/** Rows the viewport shows before it scrolls; it reserves their height from the first frame. */
|
|
41
41
|
const VIEWPORT_ROWS = 8;
|
|
42
|
-
/** A compact row
|
|
43
|
-
const ROW_REM =
|
|
42
|
+
/** A compact DataTable row and its totals row, and the quieter header row above them. */
|
|
43
|
+
const ROW_REM = 2.5;
|
|
44
|
+
const HEADER_REM = 2.125;
|
|
45
|
+
/** DataTable draws a hairline between rows only above this many columns. */
|
|
46
|
+
const HAIRLINE_ABOVE_COLUMNS = 8;
|
|
44
47
|
|
|
45
48
|
/** Loads a view file with the viewer's access and validates it again: a file is untrusted whoever wrote it. */
|
|
46
49
|
async function loadView(url: string, path: string, signal: AbortSignal): Promise<CloudAiView | null> {
|
|
@@ -75,9 +78,16 @@ type ChartSummary = Extract<CloudAiViewSummary, { view: "chart" }>;
|
|
|
75
78
|
type GroupSummary = Extract<CloudAiViewSummary, { view: "group" }>;
|
|
76
79
|
type ExplorerSummary = Extract<CloudAiViewSummary, { view: "explorer" }>;
|
|
77
80
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The header row, the rows up to the viewport's, and the totals row when there is one, inside the paper's 1px frame.
|
|
83
|
+
* Wide tables add a 1px hairline after each visible body row with a following body row; the footer adds none.
|
|
84
|
+
*/
|
|
85
|
+
const tableHeight = (summary: TableSummary) => {
|
|
86
|
+
const bodyRows = Math.max(1, Math.min(summary.rows, VIEWPORT_ROWS));
|
|
87
|
+
const rows = bodyRows + Number(summary.footer);
|
|
88
|
+
const lines = summary.columns > HAIRLINE_ABOVE_COLUMNS ? bodyRows - 1 + Number(summary.rows > VIEWPORT_ROWS) : 0;
|
|
89
|
+
return `calc(${HEADER_REM + rows * ROW_REM}rem + ${2 + lines}px)`;
|
|
90
|
+
};
|
|
81
91
|
|
|
82
92
|
/**
|
|
83
93
|
* A table view: title, Copy data and a sortable table in a viewport whose height follows the row count of its
|
|
@@ -244,12 +254,17 @@ function CloudStatsView(props: { summary: StatsSummary; input: CloudAiStatsView
|
|
|
244
254
|
{(value) => (
|
|
245
255
|
<StatGrid columns={columns()}>
|
|
246
256
|
<Index each={value()}>
|
|
247
|
-
{(tile) => (
|
|
257
|
+
{(tile, index) => (
|
|
248
258
|
<StatCell
|
|
249
259
|
label={tile().label}
|
|
250
260
|
value={tile().value}
|
|
251
261
|
title={tile().value}
|
|
252
262
|
trend={tile().sparkline}
|
|
263
|
+
trendTooltip={({ datum }) => {
|
|
264
|
+
const point = tile().sparkline?.[datum.index];
|
|
265
|
+
const format = cloudAiNumberFormatters(props.input?.items[index]?.format, locale()).value;
|
|
266
|
+
return { rows: point === undefined ? [] : [{ label: tile().label, value: format(point) }] };
|
|
267
|
+
}}
|
|
253
268
|
sub={tile().sub}
|
|
254
269
|
accent={deltaAccent(tile().delta)}
|
|
255
270
|
/>
|
|
@@ -6,14 +6,25 @@ import { dates } from "@k2b/stdlib";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { coreSettings } from "../services";
|
|
8
8
|
import { normalizeWorkflowSchedule } from "../workflows/runtime";
|
|
9
|
+
import type { ChatTaskReason } from "./chat-task-reasons";
|
|
9
10
|
import type { AiChatTask, AiChatTaskOccurrence, AiChatTaskOccurrenceState, AiChatTaskSchedule, AiChatTaskState } from "./chat-tasks";
|
|
10
11
|
import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
11
12
|
|
|
12
|
-
export const ChatTaskIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character
|
|
13
|
-
export const ChatTaskOccurrenceIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character
|
|
13
|
+
export const ChatTaskIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character routine ID.");
|
|
14
|
+
export const ChatTaskOccurrenceIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character routine run ID.");
|
|
14
15
|
export const AiConversationIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character AI conversation ID.");
|
|
15
16
|
|
|
17
|
+
export const chatTaskName = (task: { title?: string | null; chatTitle?: string | null; prompt: string }): string =>
|
|
18
|
+
task.title?.trim() ||
|
|
19
|
+
task.chatTitle?.trim() ||
|
|
20
|
+
Array.from(task.prompt.split(/\n|:\s/u)[0]!.trim())
|
|
21
|
+
.slice(0, 80)
|
|
22
|
+
.join("");
|
|
23
|
+
|
|
16
24
|
export type AiChatTaskView = {
|
|
25
|
+
title: string | null;
|
|
26
|
+
name: string;
|
|
27
|
+
reason: ChatTaskReason | null;
|
|
17
28
|
nextRunAt?: string | null;
|
|
18
29
|
id: string;
|
|
19
30
|
chatId: string;
|
|
@@ -42,6 +53,12 @@ export type AiChatTaskOccurrenceView = {
|
|
|
42
53
|
};
|
|
43
54
|
|
|
44
55
|
export const toAiChatTaskView = (task: AiChatTask): AiChatTaskView => ({
|
|
56
|
+
title: task.title,
|
|
57
|
+
name: task.name,
|
|
58
|
+
reason:
|
|
59
|
+
task.reasonCode && task.reasonAt
|
|
60
|
+
? { code: task.reasonCode, at: task.reasonAt, ...(task.reasonAction ? { action: task.reasonAction } : {}) }
|
|
61
|
+
: null,
|
|
45
62
|
id: task.shortId,
|
|
46
63
|
chatId: task.chatId,
|
|
47
64
|
chatTitle: task.chatTitle,
|
|
@@ -93,12 +110,23 @@ export const getChatTaskTimezone = async (): Promise<string> =>
|
|
|
93
110
|
export const chatTaskCreateFingerprint = (input: {
|
|
94
111
|
chatId: string;
|
|
95
112
|
prompt: string;
|
|
113
|
+
title?: string;
|
|
96
114
|
schedule: z.infer<typeof ChatTaskScheduleInputSchema>;
|
|
97
115
|
timezone?: string;
|
|
98
116
|
grants?: TaskGrant[];
|
|
99
117
|
}): string =>
|
|
100
118
|
new Bun.CryptoHasher("sha256")
|
|
101
|
-
.update(
|
|
119
|
+
.update(
|
|
120
|
+
JSON.stringify([
|
|
121
|
+
input.chatId,
|
|
122
|
+
input.prompt.trim(),
|
|
123
|
+
input.schedule,
|
|
124
|
+
input.timezone ?? null,
|
|
125
|
+
input.grants ?? [],
|
|
126
|
+
// Without a title the hash stays as before titles existed, so earlier idempotency keys still replay.
|
|
127
|
+
...(input.title?.trim() ? [input.title.trim()] : []),
|
|
128
|
+
]),
|
|
129
|
+
)
|
|
102
130
|
.digest("hex");
|
|
103
131
|
|
|
104
132
|
export const normalizeChatTaskSchedule = async (
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import { canonicalLocale } from "../shared/locale";
|
|
3
|
+
|
|
4
|
+
/** Called in a transaction; new columns and reason repairs commit together. */
|
|
5
|
+
export const migrateChatTaskReasons = async (sql: SQL): Promise<void> => {
|
|
6
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_code TEXT`.simple();
|
|
7
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_action TEXT`.simple();
|
|
8
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_at TIMESTAMPTZ`.simple();
|
|
9
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS title TEXT`.simple();
|
|
10
|
+
const [column] = await sql`SELECT 1 FROM information_schema.columns
|
|
11
|
+
WHERE table_schema = 'ai' AND table_name = 'chat_tasks' AND column_name = 'locale'`;
|
|
12
|
+
if (!column) {
|
|
13
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS locale TEXT`.simple();
|
|
14
|
+
// Data-sized work runs only with the new column. A restart never rescans old turns.
|
|
15
|
+
let cursor: string | null = null;
|
|
16
|
+
while (true) {
|
|
17
|
+
const rows: { id: string; locale: string | null }[] = await sql`
|
|
18
|
+
SELECT task.id, COALESCE(
|
|
19
|
+
(SELECT turn.run_config->>'locale' FROM ai.turns turn
|
|
20
|
+
WHERE turn.conversation_id = task.conversation_id AND turn.run_config->>'kind' = 'chat'
|
|
21
|
+
AND NOT (turn.run_config ? 'background') AND turn.created_at <= task.created_at
|
|
22
|
+
ORDER BY turn.created_at DESC, turn.id DESC LIMIT 1),
|
|
23
|
+
(SELECT turn.run_config->>'locale' FROM ai.turns turn
|
|
24
|
+
WHERE turn.conversation_id = task.conversation_id AND turn.run_config->>'kind' = 'chat'
|
|
25
|
+
AND NOT (turn.run_config ? 'background')
|
|
26
|
+
ORDER BY turn.created_at DESC, turn.id DESC LIMIT 1)) AS locale
|
|
27
|
+
FROM ai.chat_tasks task WHERE (${cursor}::uuid IS NULL OR task.id > ${cursor}::uuid)
|
|
28
|
+
ORDER BY task.id LIMIT 500`;
|
|
29
|
+
if (!rows.length) break;
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
const locale = canonicalLocale(row.locale);
|
|
32
|
+
if (locale) await sql`UPDATE ai.chat_tasks SET locale = ${locale} WHERE id = ${row.id}::uuid`;
|
|
33
|
+
}
|
|
34
|
+
cursor = rows[rows.length - 1]!.id;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await sql`
|
|
38
|
+
DO $$ BEGIN
|
|
39
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_reason_code_check') THEN
|
|
40
|
+
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_reason_code_check
|
|
41
|
+
CHECK (reason_code IN ('paused', 'chat_archived', 'owner_inactive', 'failed', 'rights_changed', 'needs_approval'));
|
|
42
|
+
END IF;
|
|
43
|
+
-- Bounded only: an older Core clears reason_code without knowing reason_action, so readers pair the two.
|
|
44
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_reason_action_length_check') THEN
|
|
45
|
+
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_reason_action_length_check
|
|
46
|
+
CHECK (reason_action IS NULL OR char_length(reason_action) BETWEEN 1 AND 200);
|
|
47
|
+
END IF;
|
|
48
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_title_check') THEN
|
|
49
|
+
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_title_check CHECK (char_length(btrim(title)) BETWEEN 1 AND 80);
|
|
50
|
+
END IF;
|
|
51
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_reason_state_check') THEN
|
|
52
|
+
-- Repair legacy and rollback writes, preserving valid reasons and their timestamps.
|
|
53
|
+
UPDATE ai.chat_tasks SET reason_code = CASE
|
|
54
|
+
WHEN state NOT IN ('paused', 'needs_attention') THEN NULL
|
|
55
|
+
WHEN state = 'paused' THEN 'paused'
|
|
56
|
+
WHEN last_error IN ('Scheduled task mandate revision changed', 'Scheduled task mandate or sponsor is unavailable') THEN 'rights_changed'
|
|
57
|
+
ELSE 'failed' END,
|
|
58
|
+
reason_action = NULL,
|
|
59
|
+
reason_at = CASE WHEN state IN ('paused', 'needs_attention') THEN updated_at ELSE NULL END
|
|
60
|
+
WHERE NOT (
|
|
61
|
+
(state IN ('paused', 'needs_attention')) = (reason_code IS NOT NULL)
|
|
62
|
+
AND (reason_code IS NULL OR
|
|
63
|
+
(state = 'paused' AND reason_code IN ('paused', 'chat_archived', 'owner_inactive')) OR
|
|
64
|
+
(state = 'needs_attention' AND reason_code IN ('failed', 'rights_changed', 'needs_approval')))
|
|
65
|
+
);
|
|
66
|
+
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_reason_state_check CHECK (
|
|
67
|
+
(state IN ('paused', 'needs_attention')) = (reason_code IS NOT NULL)
|
|
68
|
+
AND (reason_code IS NULL OR
|
|
69
|
+
(state = 'paused' AND reason_code IN ('paused', 'chat_archived', 'owner_inactive')) OR
|
|
70
|
+
(state = 'needs_attention' AND reason_code IN ('failed', 'rights_changed', 'needs_approval')))
|
|
71
|
+
);
|
|
72
|
+
END IF;
|
|
73
|
+
END $$
|
|
74
|
+
`.simple();
|
|
75
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { dates, i18n } from "@k2b/stdlib";
|
|
2
|
+
import { canonicalLocale } from "../shared/locale";
|
|
3
|
+
|
|
4
|
+
export const CHAT_TASK_REASON_CODES = ["paused", "chat_archived", "owner_inactive", "failed", "rights_changed", "needs_approval"] as const;
|
|
5
|
+
export type ChatTaskReasonCode = (typeof CHAT_TASK_REASON_CODES)[number];
|
|
6
|
+
export type ChatTaskReason = { code: ChatTaskReasonCode; at: string; action?: string };
|
|
7
|
+
|
|
8
|
+
export const chatTaskReasonState = (code: ChatTaskReasonCode): "paused" | "needs_attention" => {
|
|
9
|
+
switch (code) {
|
|
10
|
+
case "paused":
|
|
11
|
+
case "chat_archived":
|
|
12
|
+
case "owner_inactive":
|
|
13
|
+
return "paused";
|
|
14
|
+
case "failed":
|
|
15
|
+
case "rights_changed":
|
|
16
|
+
case "needs_approval":
|
|
17
|
+
return "needs_attention";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const messages = i18n.define({
|
|
22
|
+
baseLocale: "en",
|
|
23
|
+
messages: {
|
|
24
|
+
en: {
|
|
25
|
+
paused: ({ time }: { time: string }) => `Paused by you on ${time}. What happens meanwhile is not caught up automatically.`,
|
|
26
|
+
chat_archived: "Paused because the chat is archived.",
|
|
27
|
+
owner_inactive: "Paused because the account is inactive.",
|
|
28
|
+
failed: "The last run failed.",
|
|
29
|
+
rights_changed: "What it may do changed. Please confirm again.",
|
|
30
|
+
needs_approval: ({ action }: { action?: string }) =>
|
|
31
|
+
`${action ? `Tried to ${action.replace(/^\p{Lu}(?=\p{Ll})/u, (first) => first.toLowerCase())}. ` : ""}That needs your approval every time, and routines never ask.`,
|
|
32
|
+
stopped: ({ list }: { list: string }) =>
|
|
33
|
+
list
|
|
34
|
+
? `Stopped while the routine was running. Already done: ${list}. Nothing was undone.`
|
|
35
|
+
: "Stopped while the routine was running. Nothing was undone.",
|
|
36
|
+
},
|
|
37
|
+
de: {
|
|
38
|
+
paused: ({ time }) => `Von dir pausiert am ${time}. Was in der Pause passiert, wird nicht von selbst nachgeholt.`,
|
|
39
|
+
chat_archived: "Pausiert, weil der Chat archiviert ist.",
|
|
40
|
+
owner_inactive: "Pausiert, weil das Konto inaktiv ist.",
|
|
41
|
+
failed: "Der letzte Lauf ist fehlgeschlagen.",
|
|
42
|
+
rights_changed: "Was sie darf, hat sich geändert. Bitte neu bestätigen.",
|
|
43
|
+
needs_approval: ({ action }) => `${action ? `Wollte ${action}. ` : ""}Das macht der Assistant nur mit dir, und Routinen fragen nie.`,
|
|
44
|
+
stopped: ({ list }) =>
|
|
45
|
+
list
|
|
46
|
+
? `Angehalten, während die Routine lief. Schon erledigt: ${list}. Nichts wurde rückgängig gemacht.`
|
|
47
|
+
: "Angehalten, während die Routine lief. Nichts wurde rückgängig gemacht.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A one-time routine resumes only before its time: a run at that time already happened or was stopped, and what was
|
|
54
|
+
* missed in a pause is not caught up. It needs a new time first.
|
|
55
|
+
*/
|
|
56
|
+
export const chatTaskNeedsNewTime = (
|
|
57
|
+
task: {
|
|
58
|
+
state: "active" | "paused" | "completed" | "needs_attention";
|
|
59
|
+
schedule: { kind: "once"; runAt: string } | { kind: "cron"; cron: string };
|
|
60
|
+
},
|
|
61
|
+
now: Date = new Date(),
|
|
62
|
+
): boolean =>
|
|
63
|
+
task.state !== "active" &&
|
|
64
|
+
task.schedule.kind === "once" &&
|
|
65
|
+
(task.state === "needs_attention" || Date.parse(task.schedule.runAt) <= now.getTime());
|
|
66
|
+
|
|
67
|
+
export const chatTaskLocale = (taskLocale: string | null | undefined, appLocale?: string | null): string =>
|
|
68
|
+
canonicalLocale(taskLocale) ?? canonicalLocale(appLocale) ?? "en";
|
|
69
|
+
|
|
70
|
+
export const chatTaskReasonSentence = (reason: ChatTaskReason, context: { locale: string; timeZone: string }): string => {
|
|
71
|
+
const t = messages.resolve([context.locale]).t;
|
|
72
|
+
// The record is exhaustive: a new reason must have a sentence in both catalogs.
|
|
73
|
+
const sentences: Record<ChatTaskReasonCode, () => string> = {
|
|
74
|
+
paused: () => t.paused({ time: dates.formatDateTime(reason.at, context) }),
|
|
75
|
+
chat_archived: () => t.chat_archived,
|
|
76
|
+
owner_inactive: () => t.owner_inactive,
|
|
77
|
+
failed: () => t.failed,
|
|
78
|
+
rights_changed: () => t.rights_changed,
|
|
79
|
+
needs_approval: () => t.needs_approval({ action: reason.action }),
|
|
80
|
+
};
|
|
81
|
+
return sentences[reason.code]();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// A scan from the end instead of /[.!?]+$/, which backtracks quadratically on long runs of punctuation.
|
|
85
|
+
const withoutClosingPunctuation = (text: string): string => {
|
|
86
|
+
let end = text.length;
|
|
87
|
+
while (end > 0 && ".!?".includes(text[end - 1]!)) end -= 1;
|
|
88
|
+
return text.slice(0, end);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const chatTaskStopSentence = (done: readonly string[], locale: string): string =>
|
|
92
|
+
messages.resolve([locale]).t.stopped({ list: done.map(withoutClosingPunctuation).join("; ") });
|