okengine 0.22.0 → 0.23.1
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/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decisions list and review queue. A Flows route, not a sidebar module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Link, useSearch } from "@tanstack/react-router";
|
|
6
|
+
import { useCallback, useEffect, useState, type JSX } from "react";
|
|
7
|
+
import {
|
|
8
|
+
decisionList,
|
|
9
|
+
decisionQueue,
|
|
10
|
+
decisionResolve,
|
|
11
|
+
type DecisionLabelFailure,
|
|
12
|
+
type DecisionListRow,
|
|
13
|
+
type DecisionQueueRow,
|
|
14
|
+
} from "@/client.ts";
|
|
15
|
+
import {
|
|
16
|
+
EXPLORER_COUNT_CLASS,
|
|
17
|
+
EXPLORER_PAGE_CLASS,
|
|
18
|
+
EXPLORER_ROW_CLASS,
|
|
19
|
+
EXPLORER_STRIP_CLASS,
|
|
20
|
+
SECTION_HEAD_CLASS,
|
|
21
|
+
} from "@/components/explorer/explorer-chrome.ts";
|
|
22
|
+
import { decisionStateLabel, decisionValuesValid, resolveDecisionWithRetry } from "./resolve.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format a pending age in seconds or minutes.
|
|
26
|
+
*
|
|
27
|
+
* @param ageMs - Milliseconds since the review was requested
|
|
28
|
+
*/
|
|
29
|
+
function formatAge(ageMs: number): string {
|
|
30
|
+
const seconds = Math.max(0, Math.round(ageMs / 1000));
|
|
31
|
+
if (seconds < 60) return `${seconds}s`;
|
|
32
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decisions catalogue and the review queue.
|
|
37
|
+
*/
|
|
38
|
+
export function DecisionsPage(): JSX.Element {
|
|
39
|
+
const [decisions, setDecisions] = useState<readonly DecisionListRow[]>([]);
|
|
40
|
+
const [failures, setFailures] = useState<readonly DecisionLabelFailure[]>([]);
|
|
41
|
+
const [rows, setRows] = useState<readonly DecisionQueueRow[]>([]);
|
|
42
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
43
|
+
const selected = (useSearch({ strict: false }) as { decision?: string }).decision;
|
|
44
|
+
const [values, setValues] = useState<Record<string, Record<string, string>>>({});
|
|
45
|
+
|
|
46
|
+
const reload = useCallback(async () => {
|
|
47
|
+
const list = await decisionList();
|
|
48
|
+
if (list.data) {
|
|
49
|
+
setDecisions(list.data.decisions);
|
|
50
|
+
setFailures(list.data.failures ?? []);
|
|
51
|
+
}
|
|
52
|
+
const queue = await decisionQueue();
|
|
53
|
+
if (queue.data) setRows(queue.data.rows);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
void reload();
|
|
58
|
+
}, [reload]);
|
|
59
|
+
|
|
60
|
+
async function approve(row: DecisionQueueRow): Promise<void> {
|
|
61
|
+
const draft = values[row.id] ?? {};
|
|
62
|
+
const submitted: Record<string, unknown> = {};
|
|
63
|
+
for (const question of row.questions) {
|
|
64
|
+
const raw = draft[question.id];
|
|
65
|
+
if (question.kind === "boolean") {
|
|
66
|
+
if (raw !== "true" && raw !== "false") continue;
|
|
67
|
+
submitted[question.id] = raw === "true";
|
|
68
|
+
} else if (raw !== undefined) submitted[question.id] = raw;
|
|
69
|
+
}
|
|
70
|
+
if (!decisionValuesValid(row.questions, submitted)) {
|
|
71
|
+
setNotice("ValidationError");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = await resolveDecisionWithRetry(decisionResolve, {
|
|
75
|
+
id: row.id,
|
|
76
|
+
values: submitted,
|
|
77
|
+
...(row.labelOnly ? { labelOnly: true } : {}),
|
|
78
|
+
});
|
|
79
|
+
if (result.error?.code === "Conflict") setNotice("Conflict");
|
|
80
|
+
else if (result.error?.code === "JournalLeaseBusy") setNotice("JournalLeaseBusy");
|
|
81
|
+
else if (result.error) setNotice(result.error.code);
|
|
82
|
+
else setNotice(null);
|
|
83
|
+
await reload();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={EXPLORER_PAGE_CLASS} data-slot="decisions-page">
|
|
88
|
+
<div className={EXPLORER_STRIP_CLASS}>
|
|
89
|
+
<Link to="/flows" className={`${SECTION_HEAD_CLASS} px-2.5`}>
|
|
90
|
+
Flows
|
|
91
|
+
</Link>
|
|
92
|
+
<span className={`${SECTION_HEAD_CLASS} px-2.5`}>Decisions</span>
|
|
93
|
+
</div>
|
|
94
|
+
{failures.length > 0 ? (
|
|
95
|
+
<ul>
|
|
96
|
+
{failures.map((failure) => (
|
|
97
|
+
<li
|
|
98
|
+
key={`${failure.at}:${failure.decision}:${failure.question}`}
|
|
99
|
+
className={EXPLORER_ROW_CLASS}
|
|
100
|
+
data-slot="decision-label-error"
|
|
101
|
+
>
|
|
102
|
+
<span className="min-w-0 flex-1 truncate">
|
|
103
|
+
{failure.decision} · {failure.question}
|
|
104
|
+
</span>
|
|
105
|
+
<span className={EXPLORER_COUNT_CLASS}>{failure.message}</span>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
) : null}
|
|
110
|
+
<ul>
|
|
111
|
+
{decisions.map((decision) => (
|
|
112
|
+
<li
|
|
113
|
+
key={decision.name}
|
|
114
|
+
className={EXPLORER_ROW_CLASS}
|
|
115
|
+
data-state={decisionStateLabel(decision.state)}
|
|
116
|
+
data-decision={decision.name}
|
|
117
|
+
data-selected={selected === decision.name ? "true" : "false"}
|
|
118
|
+
>
|
|
119
|
+
<span className="min-w-0 flex-1 truncate">{decision.name}</span>
|
|
120
|
+
<span className={EXPLORER_COUNT_CLASS}>{decisionStateLabel(decision.state)}</span>
|
|
121
|
+
{decision.metrics ? (
|
|
122
|
+
<span className={EXPLORER_COUNT_CLASS} data-slot="decision-metrics">
|
|
123
|
+
{Object.entries(decision.metrics)
|
|
124
|
+
.map(([key, value]) => `${key} ${value}`)
|
|
125
|
+
.join(" · ")}
|
|
126
|
+
</span>
|
|
127
|
+
) : null}
|
|
128
|
+
{decision.promote ? (
|
|
129
|
+
<code className="text-xs" data-slot="decision-promote">
|
|
130
|
+
{decision.promote}
|
|
131
|
+
</code>
|
|
132
|
+
) : null}
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
<div className={EXPLORER_STRIP_CLASS}>
|
|
137
|
+
<span className={`${SECTION_HEAD_CLASS} px-2.5`}>Review queue</span>
|
|
138
|
+
</div>
|
|
139
|
+
{notice ? (
|
|
140
|
+
<p className="px-2.5 py-1 text-xs" data-slot="decision-notice">
|
|
141
|
+
{notice}
|
|
142
|
+
</p>
|
|
143
|
+
) : null}
|
|
144
|
+
<ul>
|
|
145
|
+
{rows.map((row) => (
|
|
146
|
+
<li
|
|
147
|
+
key={`${row.labelOnly ? "label" : "park"}:${row.id}`}
|
|
148
|
+
className={EXPLORER_ROW_CLASS}
|
|
149
|
+
data-label-only={row.labelOnly ? "true" : "false"}
|
|
150
|
+
>
|
|
151
|
+
<span className="min-w-0 flex-1 truncate">
|
|
152
|
+
{row.decision}
|
|
153
|
+
{row.labelOnly ? " · label" : ""}
|
|
154
|
+
</span>
|
|
155
|
+
<span className={EXPLORER_COUNT_CLASS} data-slot="decision-age">
|
|
156
|
+
{formatAge(row.ageMs)}
|
|
157
|
+
</span>
|
|
158
|
+
<form
|
|
159
|
+
className="flex items-center gap-2"
|
|
160
|
+
onSubmit={(event) => {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
void approve(row);
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{row.questions.map((question) =>
|
|
166
|
+
question.kind === "boolean" ? (
|
|
167
|
+
<select
|
|
168
|
+
key={question.id}
|
|
169
|
+
aria-label={question.id}
|
|
170
|
+
value={values[row.id]?.[question.id] ?? ""}
|
|
171
|
+
onChange={(event) => {
|
|
172
|
+
const next = event.target.value;
|
|
173
|
+
setValues((current) => ({
|
|
174
|
+
...current,
|
|
175
|
+
[row.id]: { ...current[row.id], [question.id]: next },
|
|
176
|
+
}));
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<option value="">Select</option>
|
|
180
|
+
<option value="true">true</option>
|
|
181
|
+
<option value="false">false</option>
|
|
182
|
+
</select>
|
|
183
|
+
) : (
|
|
184
|
+
<select
|
|
185
|
+
key={question.id}
|
|
186
|
+
aria-label={question.id}
|
|
187
|
+
value={values[row.id]?.[question.id] ?? ""}
|
|
188
|
+
onChange={(event) => {
|
|
189
|
+
const next = event.target.value;
|
|
190
|
+
setValues((current) => ({
|
|
191
|
+
...current,
|
|
192
|
+
[row.id]: { ...current[row.id], [question.id]: next },
|
|
193
|
+
}));
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<option value="">Select</option>
|
|
197
|
+
{(question.kind === "choice" ? question.options : question.levels)?.map(
|
|
198
|
+
(option) => (
|
|
199
|
+
<option key={option} value={option}>
|
|
200
|
+
{option}
|
|
201
|
+
</option>
|
|
202
|
+
),
|
|
203
|
+
)}
|
|
204
|
+
</select>
|
|
205
|
+
),
|
|
206
|
+
)}
|
|
207
|
+
<button type="submit" className="text-xs" disabled={row.status !== "pending"}>
|
|
208
|
+
Approve
|
|
209
|
+
</button>
|
|
210
|
+
</form>
|
|
211
|
+
</li>
|
|
212
|
+
))}
|
|
213
|
+
</ul>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decisions page resolve checks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { decisionStateLabel, decisionValuesValid, resolveDecisionWithRetry } from "./resolve.ts";
|
|
7
|
+
|
|
8
|
+
describe("decision resolve form", () => {
|
|
9
|
+
test("choice and score values must be one of the declared options or levels", () => {
|
|
10
|
+
const questions = [
|
|
11
|
+
{ id: "team", kind: "choice" as const, options: ["billing", "none_of_these"] },
|
|
12
|
+
{ id: "rank", kind: "score" as const, levels: ["low", "high"] },
|
|
13
|
+
];
|
|
14
|
+
expect(decisionValuesValid(questions, { team: "billing", rank: "low" })).toBe(true);
|
|
15
|
+
expect(decisionValuesValid(questions, { team: "other", rank: "low" })).toBe(false);
|
|
16
|
+
expect(decisionValuesValid(questions, { team: "billing", rank: "mid" })).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("candidate ready is the label for a fitted candidate", () => {
|
|
20
|
+
expect(decisionStateLabel("candidate")).toBe("candidate ready");
|
|
21
|
+
expect(decisionStateLabel("suspended")).toBe("suspended");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a lease collision retries and a conflict does not", async () => {
|
|
25
|
+
let calls = 0;
|
|
26
|
+
const leased = await resolveDecisionWithRetry(
|
|
27
|
+
async () => {
|
|
28
|
+
calls += 1;
|
|
29
|
+
if (calls < 3) return { error: { code: "JournalLeaseBusy" } };
|
|
30
|
+
return { data: { ok: true as const } };
|
|
31
|
+
},
|
|
32
|
+
{ id: "1", values: { team: "billing" } },
|
|
33
|
+
);
|
|
34
|
+
expect(leased.data?.ok).toBe(true);
|
|
35
|
+
expect(calls).toBe(3);
|
|
36
|
+
|
|
37
|
+
let conflictCalls = 0;
|
|
38
|
+
const conflict = await resolveDecisionWithRetry(
|
|
39
|
+
async () => {
|
|
40
|
+
conflictCalls += 1;
|
|
41
|
+
return { error: { code: "Conflict" } };
|
|
42
|
+
},
|
|
43
|
+
{ id: "1", values: { team: "billing" } },
|
|
44
|
+
);
|
|
45
|
+
expect(conflict.error?.code).toBe("Conflict");
|
|
46
|
+
expect(conflictCalls).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve-form checks and lease retries for the decisions page.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** One question the form can answer. */
|
|
6
|
+
export interface DecisionFormQuestion {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly kind: "boolean" | "choice" | "score";
|
|
9
|
+
readonly options?: readonly string[];
|
|
10
|
+
readonly levels?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Result of one resolve POST. */
|
|
14
|
+
export interface DecisionResolveResult {
|
|
15
|
+
readonly error?: {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly data?: unknown;
|
|
18
|
+
} | null;
|
|
19
|
+
readonly data?: { readonly ok: true } | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when every open question has a value inside its options or levels.
|
|
24
|
+
*
|
|
25
|
+
* @param questions - Open questions from the queue row
|
|
26
|
+
* @param values - Form values
|
|
27
|
+
*/
|
|
28
|
+
export function decisionValuesValid(
|
|
29
|
+
questions: readonly DecisionFormQuestion[],
|
|
30
|
+
values: Readonly<Record<string, unknown>>,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (Object.keys(values).length !== questions.length) return false;
|
|
33
|
+
for (const question of questions) {
|
|
34
|
+
const value = values[question.id];
|
|
35
|
+
if (question.kind === "boolean") {
|
|
36
|
+
if (typeof value !== "boolean") return false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value !== "string") return false;
|
|
40
|
+
if (question.kind === "choice") {
|
|
41
|
+
if (!question.options?.includes(value)) return false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!question.levels?.includes(value)) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* POST a resolve, retrying `JournalLeaseBusy`. `Conflict` returns immediately.
|
|
51
|
+
*
|
|
52
|
+
* @param post - Resolve client
|
|
53
|
+
* @param body - Review id and values
|
|
54
|
+
* @param attempts - How many times to try
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveDecisionWithRetry(
|
|
57
|
+
post: (body: {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
60
|
+
readonly labelOnly?: boolean;
|
|
61
|
+
}) => Promise<DecisionResolveResult>,
|
|
62
|
+
body: {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
65
|
+
readonly labelOnly?: boolean;
|
|
66
|
+
},
|
|
67
|
+
attempts = 5,
|
|
68
|
+
): Promise<DecisionResolveResult> {
|
|
69
|
+
let last: DecisionResolveResult = {};
|
|
70
|
+
for (let i = 0; i < attempts; i++) {
|
|
71
|
+
last = await post(body);
|
|
72
|
+
if (last.error?.code !== "JournalLeaseBusy") return last;
|
|
73
|
+
const waitMs = retryAfterToMs(retryAfterValue(last.error?.data));
|
|
74
|
+
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
75
|
+
}
|
|
76
|
+
return last;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Label shown for a list state. */
|
|
80
|
+
export function decisionStateLabel(
|
|
81
|
+
state: "learning" | "candidate" | "certified" | "suspended",
|
|
82
|
+
): string {
|
|
83
|
+
if (state === "candidate") return "candidate ready";
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read `retryAfter` from a resolve error body.
|
|
89
|
+
*
|
|
90
|
+
* @param data - Error payload
|
|
91
|
+
*/
|
|
92
|
+
function retryAfterValue(data: unknown): number | string | undefined {
|
|
93
|
+
if (!data || typeof data !== "object" || !("retryAfter" in data)) return undefined;
|
|
94
|
+
const retryAfter = data.retryAfter;
|
|
95
|
+
return typeof retryAfter === "number" || typeof retryAfter === "string" ? retryAfter : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `Retry-After` as delta-seconds or an HTTP-date.
|
|
100
|
+
*
|
|
101
|
+
* @param value - Seconds, or an HTTP-date string
|
|
102
|
+
*/
|
|
103
|
+
function retryAfterToMs(value: number | string | undefined): number {
|
|
104
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, value) * 1000;
|
|
105
|
+
if (typeof value !== "string" || value.length === 0) return 0;
|
|
106
|
+
const seconds = Number(value);
|
|
107
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds) * 1000;
|
|
108
|
+
const at = Date.parse(value);
|
|
109
|
+
if (Number.isNaN(at)) return 0;
|
|
110
|
+
return Math.max(0, at - Date.now());
|
|
111
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Link } from "@tanstack/react-router";
|
|
6
7
|
import { ExplorerStartToggle } from "@/components/explorer/explorer-start-toggle.tsx";
|
|
7
8
|
import { EXPLORER_PAGE_CLASS, EXPLORER_SPLIT } from "@/components/explorer/explorer-chrome.ts";
|
|
8
9
|
import { useExplorerStartPanel } from "@/components/explorer/use-explorer-start-panel.ts";
|
|
@@ -42,6 +43,10 @@ export function FlowsPage() {
|
|
|
42
43
|
|
|
43
44
|
const [graphFilter, setGraphFilter] = useState<GraphFilter | null>(null);
|
|
44
45
|
const [focusEffectIndex, setFocusEffectIndex] = useState<number | null>(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setFocusEffectIndex(null);
|
|
49
|
+
}, [selectedRunId]);
|
|
45
50
|
const [playbackKey, setPlaybackKey] = useState(0);
|
|
46
51
|
const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
|
|
47
52
|
const playbackTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -167,6 +172,11 @@ export function FlowsPage() {
|
|
|
167
172
|
|
|
168
173
|
return (
|
|
169
174
|
<div className={EXPLORER_PAGE_CLASS} data-slot="flows-page">
|
|
175
|
+
<div className="flex justify-end px-3 py-1">
|
|
176
|
+
<Link to="/flows/decisions" className="text-sm underline">
|
|
177
|
+
Decisions
|
|
178
|
+
</Link>
|
|
179
|
+
</div>
|
|
170
180
|
<ResizablePanelGroup orientation="horizontal" className="min-h-0 flex-1">
|
|
171
181
|
<ResizablePanel
|
|
172
182
|
panelRef={start.panelRef}
|
|
@@ -27,6 +27,7 @@ export const EFFECT_KIND_LABEL: Readonly<Record<RunEffectKind, string>> = {
|
|
|
27
27
|
secret: "Secret",
|
|
28
28
|
call: "Call",
|
|
29
29
|
fetch: "Fetch",
|
|
30
|
+
decide: "Decide",
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -46,6 +47,7 @@ export function effectKindSummaryLabel(kind: RunEffectKind, count: number): stri
|
|
|
46
47
|
secret: "secret",
|
|
47
48
|
call: "call",
|
|
48
49
|
fetch: "fetch",
|
|
50
|
+
decide: "decide",
|
|
49
51
|
};
|
|
50
52
|
const plural: Record<RunEffectKind, string> = {
|
|
51
53
|
read: "reads",
|
|
@@ -57,6 +59,7 @@ export function effectKindSummaryLabel(kind: RunEffectKind, count: number): stri
|
|
|
57
59
|
secret: "secrets",
|
|
58
60
|
call: "calls",
|
|
59
61
|
fetch: "fetches",
|
|
62
|
+
decide: "decides",
|
|
60
63
|
};
|
|
61
64
|
return `${count} ${count === 1 ? singular[kind] : plural[kind]}`;
|
|
62
65
|
}
|
|
@@ -84,6 +87,8 @@ export function effectBarColor(kind: RunEffectKind): string {
|
|
|
84
87
|
case "send":
|
|
85
88
|
case "fetch":
|
|
86
89
|
return EDGE_STROKE.sends;
|
|
90
|
+
case "decide":
|
|
91
|
+
return EDGE_STROKE.asks;
|
|
87
92
|
case "secret":
|
|
88
93
|
return EDGE_STROKE.secrets;
|
|
89
94
|
}
|
|
@@ -108,6 +113,7 @@ export function effectKindIcon(kind: RunEffectKind, resource?: string): ElementH
|
|
|
108
113
|
return ELEMENT_ICONS.flow.icon;
|
|
109
114
|
case "ask":
|
|
110
115
|
case "embed":
|
|
116
|
+
case "decide":
|
|
111
117
|
return ELEMENT_ICONS.ai.icon;
|
|
112
118
|
case "send":
|
|
113
119
|
case "fetch":
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { effectAiHref } from "./effect-summary.ts";
|
|
3
|
+
|
|
4
|
+
describe("effectAiHref", () => {
|
|
5
|
+
const agents = new Set(["planner"]);
|
|
6
|
+
|
|
7
|
+
test("an agent run id opens that run", () => {
|
|
8
|
+
expect(effectAiHref({ kind: "ask", resource: "planner", agentRunId: "run-9" }, agents)).toBe(
|
|
9
|
+
"/observability?view=ai&agentRun=run-9",
|
|
10
|
+
);
|
|
11
|
+
expect(effectAiHref({ kind: "call", resource: "planner", agentRunId: "run-9" }, agents)).toBe(
|
|
12
|
+
"/observability?view=ai&agentRun=run-9",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("without a run id the link searches by name", () => {
|
|
17
|
+
expect(effectAiHref({ kind: "ask", resource: "weekly-summary@1" }, agents)).toBe(
|
|
18
|
+
"/observability?view=ai&q=weekly-summary",
|
|
19
|
+
);
|
|
20
|
+
expect(effectAiHref({ kind: "call", resource: "planner" }, agents)).toBe(
|
|
21
|
+
"/observability?view=ai&q=planner",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -46,6 +46,7 @@ const KIND_ORDER: readonly RunEffectKind[] = [
|
|
|
46
46
|
"embed",
|
|
47
47
|
"send",
|
|
48
48
|
"fetch",
|
|
49
|
+
"decide",
|
|
49
50
|
"secret",
|
|
50
51
|
];
|
|
51
52
|
|
|
@@ -187,6 +188,28 @@ export function isMcpResource(resource: string): boolean {
|
|
|
187
188
|
return parseMcpToolRef(resource) !== null;
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Where an ask, decide, or agent call row opens.
|
|
193
|
+
*
|
|
194
|
+
* @param effect - Effect kind and resource
|
|
195
|
+
* @param agents - Declared agent names. A `call` links only when it names one.
|
|
196
|
+
*/
|
|
197
|
+
export function effectAiHref(
|
|
198
|
+
effect: Pick<RunEffect, "kind" | "resource"> & { readonly agentRunId?: string },
|
|
199
|
+
agents: ReadonlySet<string>,
|
|
200
|
+
): string | null {
|
|
201
|
+
const name = effect.resource.split("@")[0] ?? effect.resource;
|
|
202
|
+
if (effect.kind === "decide") {
|
|
203
|
+
return `/flows/decisions?decision=${encodeURIComponent(name)}`;
|
|
204
|
+
}
|
|
205
|
+
const agentCall = effect.kind === "ask" || (effect.kind === "call" && agents.has(name));
|
|
206
|
+
if (!agentCall) return null;
|
|
207
|
+
if (effect.agentRunId) {
|
|
208
|
+
return `/observability?view=ai&agentRun=${encodeURIComponent(effect.agentRunId)}`;
|
|
209
|
+
}
|
|
210
|
+
return `/observability?view=ai&q=${encodeURIComponent(name)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
export function effectEventLabel(effect: Pick<RunEffect, "kind" | "resource">): string {
|
|
191
214
|
if (isComputedCacheKey(effect.resource)) {
|
|
192
215
|
return "Cache read";
|
|
@@ -207,6 +230,7 @@ export function effectEventLabel(effect: Pick<RunEffect, "kind" | "resource">):
|
|
|
207
230
|
secret: "Secret",
|
|
208
231
|
call: "Call",
|
|
209
232
|
fetch: "Fetch",
|
|
233
|
+
decide: "Decide",
|
|
210
234
|
};
|
|
211
235
|
return labels[effect.kind];
|
|
212
236
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status accent for the trace Response frame.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const STATUS_REASON: Record<number, string> = {
|
|
6
|
+
200: "OK",
|
|
7
|
+
201: "Created",
|
|
8
|
+
202: "Accepted",
|
|
9
|
+
204: "No Content",
|
|
10
|
+
301: "Moved",
|
|
11
|
+
302: "Found",
|
|
12
|
+
304: "Not Modified",
|
|
13
|
+
400: "Bad Request",
|
|
14
|
+
401: "Unauthorized",
|
|
15
|
+
403: "Forbidden",
|
|
16
|
+
404: "Not Found",
|
|
17
|
+
405: "Method Not Allowed",
|
|
18
|
+
409: "Conflict",
|
|
19
|
+
415: "Unsupported Media Type",
|
|
20
|
+
422: "Unprocessable Content",
|
|
21
|
+
429: "Too Many Requests",
|
|
22
|
+
500: "Internal Server Error",
|
|
23
|
+
502: "Bad Gateway",
|
|
24
|
+
503: "Service Unavailable",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Short reason phrase for a status code, when we know one.
|
|
29
|
+
*
|
|
30
|
+
* @param status - HTTP status
|
|
31
|
+
*/
|
|
32
|
+
export function httpStatusReason(status: number): string | null {
|
|
33
|
+
return STATUS_REASON[status] ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Text color for a status code.
|
|
38
|
+
*
|
|
39
|
+
* @param status - HTTP status
|
|
40
|
+
*/
|
|
41
|
+
export function httpStatusTextClass(status: number): string {
|
|
42
|
+
if (status >= 500) return "text-destructive";
|
|
43
|
+
if (status >= 400) return "text-amber-700 dark:text-amber-400";
|
|
44
|
+
if (status >= 300) return "text-sky-700 dark:text-sky-400";
|
|
45
|
+
return "text-emerald-700 dark:text-emerald-400";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rail fill for a status code.
|
|
50
|
+
*
|
|
51
|
+
* @param status - HTTP status
|
|
52
|
+
*/
|
|
53
|
+
export function httpStatusRailClass(status: number): string {
|
|
54
|
+
if (status >= 500) return "bg-destructive";
|
|
55
|
+
if (status >= 400) return "bg-amber-500";
|
|
56
|
+
if (status >= 300) return "bg-sky-500";
|
|
57
|
+
return "bg-emerald-500";
|
|
58
|
+
}
|