ltcai 3.6.0 → 4.0.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/README.md +39 -31
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/kg-schema.md +51 -53
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -520
- package/knowledge_graph.py +37 -4629
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +16 -17
- package/latticeai/api/agents.py +20 -7
- package/latticeai/api/auth.py +46 -15
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +139 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +5 -8
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +11 -16
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +85 -6
- package/latticeai/api/workspace.py +93 -57
- package/latticeai/app_factory.py +1781 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +66 -10
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +477 -29
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1536
- package/latticeai/services/agent_runtime.py +243 -4
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +47 -0
- package/latticeai/services/kg_portability.py +33 -3
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +55 -16
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +10 -20
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +105 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +33 -25
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
- package/static/v3/js/core/shell.js +66 -37
- package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
- package/static/v3/js/core/store.js +11 -1
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
- package/static/v3/js/views/knowledge-graph.js +60 -44
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/account.html +0 -115
- package/static/activity.html +0 -73
- package/static/admin.html +0 -488
- package/static/agents.html +0 -139
- package/static/chat.html +0 -844
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -124
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.2ce3815a.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, api, c } = ctx;
|
|
5
|
+
const feedHost = h("div", c.loading({ lines: 4 }));
|
|
6
|
+
const presenceHost = h("div", c.loading({ lines: 2 }));
|
|
7
|
+
const timelineHost = h("div", c.loading({ lines: 4 }));
|
|
8
|
+
|
|
9
|
+
const root = h("div.lt3-stack-6",
|
|
10
|
+
c.viewHeader({ eyebrow: t("activity.eyebrow"), title: t("activity.title"), sub: t("activity.sub") }),
|
|
11
|
+
c.panel({ title: t("activity.feed"), children: feedHost }),
|
|
12
|
+
c.panel({ title: t("activity.presence"), children: presenceHost }),
|
|
13
|
+
c.panel({ title: t("activity.timeMachine"), children: timelineHost }),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
await load();
|
|
17
|
+
wireLiveFeed();
|
|
18
|
+
return root;
|
|
19
|
+
|
|
20
|
+
async function load() {
|
|
21
|
+
const [feed, presence, timeline] = await Promise.all([api.realtimeFeed(80), api.presence(), api.timeMachine(80)]);
|
|
22
|
+
feedHost.replaceChildren(listEvents(ctx, feed.data?.events || [], feed.source));
|
|
23
|
+
presenceHost.replaceChildren(listPresence(ctx, presence.data?.presence || [], presence.source));
|
|
24
|
+
timelineHost.replaceChildren(listEvents(ctx, timeline.data?.events || [], timeline.source));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function wireLiveFeed() {
|
|
28
|
+
if (!window.EventSource) return;
|
|
29
|
+
try {
|
|
30
|
+
const stream = new EventSource("/realtime/stream");
|
|
31
|
+
stream.onmessage = () => load();
|
|
32
|
+
setTimeout(() => stream.close(), 120000);
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function listEvents(ctx, events, source) {
|
|
38
|
+
const { h, c } = ctx;
|
|
39
|
+
return h("div.lt3-stack-3",
|
|
40
|
+
h("div.lt3-row-2", c.sourceBadge(source)),
|
|
41
|
+
events.length ? c.table([
|
|
42
|
+
{ key: "event", label: t("common.status"), render: (e) => h("div", h("b", e.event_type || e.type || e.area || "event"), h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, e.payload?.run_id || e.payload?.workflow_id || e.id || "")) },
|
|
43
|
+
{ key: "area", label: t("common.type"), width: "1%", render: (e) => c.pill(e.area || e.kind || "system") },
|
|
44
|
+
{ key: "when", label: t("common.created"), width: "1%", render: (e) => h("span.lt3-faint", { style: { "white-space": "nowrap" } }, fmt(e.timestamp || e.created_at || e.at)) },
|
|
45
|
+
], events.slice(0, 50)) : c.emptyState({ icon: "activity", title: t("activity.feed"), body: t("common.none") }),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listPresence(ctx, rows, source) {
|
|
50
|
+
const { h, c } = ctx;
|
|
51
|
+
return h("div.lt3-stack-3",
|
|
52
|
+
h("div.lt3-row-2", c.sourceBadge(source)),
|
|
53
|
+
rows.length ? c.table([
|
|
54
|
+
{ key: "user", label: t("account.email"), render: (p) => p.user || p.email || p.client_id || "local" },
|
|
55
|
+
{ key: "workspace", label: "workspace_id", render: (p) => h("span.lt3-mono", p.workspace_id || "personal") },
|
|
56
|
+
{ key: "when", label: t("common.updated"), width: "1%", render: (p) => fmt(p.last_seen || p.joined_at) },
|
|
57
|
+
], rows) : c.emptyState({ icon: "users", title: t("activity.presence"), body: t("common.none") }),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fmt(ts) {
|
|
62
|
+
if (!ts) return "—";
|
|
63
|
+
try {
|
|
64
|
+
const d = typeof ts === "number" ? new Date(ts * 1000) : new Date(ts);
|
|
65
|
+
return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString();
|
|
66
|
+
} catch { return String(ts); }
|
|
67
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { t } from "../core/i18n.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, api, c } = ctx;
|
|
5
|
+
const feedHost = h("div", c.loading({ lines: 4 }));
|
|
6
|
+
const presenceHost = h("div", c.loading({ lines: 2 }));
|
|
7
|
+
const timelineHost = h("div", c.loading({ lines: 4 }));
|
|
8
|
+
|
|
9
|
+
const root = h("div.lt3-stack-6",
|
|
10
|
+
c.viewHeader({ eyebrow: t("activity.eyebrow"), title: t("activity.title"), sub: t("activity.sub") }),
|
|
11
|
+
c.panel({ title: t("activity.feed"), children: feedHost }),
|
|
12
|
+
c.panel({ title: t("activity.presence"), children: presenceHost }),
|
|
13
|
+
c.panel({ title: t("activity.timeMachine"), children: timelineHost }),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
await load();
|
|
17
|
+
wireLiveFeed();
|
|
18
|
+
return root;
|
|
19
|
+
|
|
20
|
+
async function load() {
|
|
21
|
+
const [feed, presence, timeline] = await Promise.all([api.realtimeFeed(80), api.presence(), api.timeMachine(80)]);
|
|
22
|
+
feedHost.replaceChildren(listEvents(ctx, feed.data?.events || [], feed.source));
|
|
23
|
+
presenceHost.replaceChildren(listPresence(ctx, presence.data?.presence || [], presence.source));
|
|
24
|
+
timelineHost.replaceChildren(listEvents(ctx, timeline.data?.events || [], timeline.source));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function wireLiveFeed() {
|
|
28
|
+
if (!window.EventSource) return;
|
|
29
|
+
try {
|
|
30
|
+
const stream = new EventSource("/realtime/stream");
|
|
31
|
+
stream.onmessage = () => load();
|
|
32
|
+
setTimeout(() => stream.close(), 120000);
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function listEvents(ctx, events, source) {
|
|
38
|
+
const { h, c } = ctx;
|
|
39
|
+
return h("div.lt3-stack-3",
|
|
40
|
+
h("div.lt3-row-2", c.sourceBadge(source)),
|
|
41
|
+
events.length ? c.table([
|
|
42
|
+
{ key: "event", label: t("common.status"), render: (e) => h("div", h("b", e.event_type || e.type || e.area || "event"), h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, e.payload?.run_id || e.payload?.workflow_id || e.id || "")) },
|
|
43
|
+
{ key: "area", label: t("common.type"), width: "1%", render: (e) => c.pill(e.area || e.kind || "system") },
|
|
44
|
+
{ key: "when", label: t("common.created"), width: "1%", render: (e) => h("span.lt3-faint", { style: { "white-space": "nowrap" } }, fmt(e.timestamp || e.created_at || e.at)) },
|
|
45
|
+
], events.slice(0, 50)) : c.emptyState({ icon: "activity", title: t("activity.feed"), body: t("common.none") }),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listPresence(ctx, rows, source) {
|
|
50
|
+
const { h, c } = ctx;
|
|
51
|
+
return h("div.lt3-stack-3",
|
|
52
|
+
h("div.lt3-row-2", c.sourceBadge(source)),
|
|
53
|
+
rows.length ? c.table([
|
|
54
|
+
{ key: "user", label: t("account.email"), render: (p) => p.user || p.email || p.client_id || "local" },
|
|
55
|
+
{ key: "workspace", label: "workspace_id", render: (p) => h("span.lt3-mono", p.workspace_id || "personal") },
|
|
56
|
+
{ key: "when", label: t("common.updated"), width: "1%", render: (p) => fmt(p.last_seen || p.joined_at) },
|
|
57
|
+
], rows) : c.emptyState({ icon: "users", title: t("activity.presence"), body: t("common.none") }),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fmt(ts) {
|
|
62
|
+
if (!ts) return "—";
|
|
63
|
+
try {
|
|
64
|
+
const d = typeof ts === "number" ? new Date(ts * 1000) : new Date(ts);
|
|
65
|
+
return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString();
|
|
66
|
+
} catch { return String(ts); }
|
|
67
|
+
}
|
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { timeAgo } from "../core/dom.a2773eb0.js";
|
|
10
10
|
|
|
11
|
-
const UNAVAILABLE = "not available from this read-only users view.";
|
|
12
|
-
|
|
13
11
|
export async function render(ctx) {
|
|
14
|
-
const { h, icon, api, c, toast } = ctx;
|
|
12
|
+
const { h, icon, api, c, toast, navigate } = ctx;
|
|
15
13
|
|
|
16
14
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
15
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
@@ -24,7 +22,7 @@ export async function render(ctx) {
|
|
|
24
22
|
sub: "Workspace members and access.",
|
|
25
23
|
actions: [
|
|
26
24
|
h("button.lt3-btn.lt3-btn--primary",
|
|
27
|
-
{ on: { click: () =>
|
|
25
|
+
{ on: { click: () => navigate("workspace-admin") } },
|
|
28
26
|
icon("user-plus"), "Invite user"),
|
|
29
27
|
],
|
|
30
28
|
}),
|
|
@@ -67,7 +65,7 @@ export async function render(ctx) {
|
|
|
67
65
|
title: "No members yet",
|
|
68
66
|
body: "Invite teammates to give them access to this workspace.",
|
|
69
67
|
action: h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm",
|
|
70
|
-
{ on: { click: () =>
|
|
68
|
+
{ on: { click: () => navigate("workspace-admin") } },
|
|
71
69
|
icon("user-plus"), "Invite user"),
|
|
72
70
|
}));
|
|
73
71
|
return;
|
|
@@ -105,7 +103,7 @@ export async function render(ctx) {
|
|
|
105
103
|
render: (r) => h("button.lt3-iconbtn.lt3-iconbtn--sm",
|
|
106
104
|
{
|
|
107
105
|
"aria-label": `Manage ${r.nickname || r.email}`,
|
|
108
|
-
on: { click: () => toast(`Manage ${r.nickname || r.email}
|
|
106
|
+
on: { click: () => toast(`Manage ${r.nickname || r.email} in Workspaces`, "info") },
|
|
109
107
|
},
|
|
110
108
|
icon("dots-vertical")),
|
|
111
109
|
},
|
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { timeAgo } from "../core/dom.js";
|
|
10
10
|
|
|
11
|
-
const UNAVAILABLE = "not available from this read-only users view.";
|
|
12
|
-
|
|
13
11
|
export async function render(ctx) {
|
|
14
|
-
const { h, icon, api, c, toast } = ctx;
|
|
12
|
+
const { h, icon, api, c, toast, navigate } = ctx;
|
|
15
13
|
|
|
16
14
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
15
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
@@ -24,7 +22,7 @@ export async function render(ctx) {
|
|
|
24
22
|
sub: "Workspace members and access.",
|
|
25
23
|
actions: [
|
|
26
24
|
h("button.lt3-btn.lt3-btn--primary",
|
|
27
|
-
{ on: { click: () =>
|
|
25
|
+
{ on: { click: () => navigate("workspace-admin") } },
|
|
28
26
|
icon("user-plus"), "Invite user"),
|
|
29
27
|
],
|
|
30
28
|
}),
|
|
@@ -67,7 +65,7 @@ export async function render(ctx) {
|
|
|
67
65
|
title: "No members yet",
|
|
68
66
|
body: "Invite teammates to give them access to this workspace.",
|
|
69
67
|
action: h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm",
|
|
70
|
-
{ on: { click: () =>
|
|
68
|
+
{ on: { click: () => navigate("workspace-admin") } },
|
|
71
69
|
icon("user-plus"), "Invite user"),
|
|
72
70
|
}));
|
|
73
71
|
return;
|
|
@@ -105,7 +103,7 @@ export async function render(ctx) {
|
|
|
105
103
|
render: (r) => h("button.lt3-iconbtn.lt3-iconbtn--sm",
|
|
106
104
|
{
|
|
107
105
|
"aria-label": `Manage ${r.nickname || r.email}`,
|
|
108
|
-
on: { click: () => toast(`Manage ${r.nickname || r.email}
|
|
106
|
+
on: { click: () => toast(`Manage ${r.nickname || r.email} in Workspaces`, "info") },
|
|
109
107
|
},
|
|
110
108
|
icon("dots-vertical")),
|
|
111
109
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* role roster enriched with real run counts, the live recent-runs ledger, and
|
|
5
5
|
* runtime health. Reports unavailable state when the runtime is unreachable.
|
|
6
6
|
* Also drives runs directly: a goal + role selection → POST /agents/api/run →
|
|
7
|
-
* a
|
|
7
|
+
* a durable async run, live logs, final status/output, queue/status, and stop.
|
|
8
8
|
* ========================================================================== */
|
|
9
9
|
|
|
10
10
|
import { timeAgo } from "../core/dom.a2773eb0.js";
|
|
@@ -37,7 +37,7 @@ export async function render(ctx) {
|
|
|
37
37
|
h("div",
|
|
38
38
|
h("div.lt3-eyebrow", "Run"),
|
|
39
39
|
h("h3.lt3-panel__title", "Run agents"),
|
|
40
|
-
h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run
|
|
40
|
+
h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run locally with durable progress and cooperative cancellation."),
|
|
41
41
|
),
|
|
42
42
|
runSrc,
|
|
43
43
|
),
|
|
@@ -134,7 +134,7 @@ function makeRunConsole(ctx, hosts) {
|
|
|
134
134
|
if (!roles.length) { ctx.toast("Select at least one role", "info"); return; }
|
|
135
135
|
|
|
136
136
|
runBtn.disabled = true;
|
|
137
|
-
runBtn.replaceChildren(c.icon("loader-2"), "
|
|
137
|
+
runBtn.replaceChildren(c.icon("loader-2"), "Starting…");
|
|
138
138
|
runSrc.replaceChildren(c.sourceBadge("pending"));
|
|
139
139
|
logsHost.replaceChildren(h("div", { style: { "margin-top": "var(--lt3-space-3)" } }, c.loading({ lines: 4 })));
|
|
140
140
|
|
|
@@ -157,18 +157,40 @@ function makeRunConsole(ctx, hosts) {
|
|
|
157
157
|
const run = data.run || {};
|
|
158
158
|
const result = data.result || {};
|
|
159
159
|
logsHost.replaceChildren(renderRunResult(run, result));
|
|
160
|
-
|
|
160
|
+
if (data.accepted && (run.id || run.run_id)) {
|
|
161
|
+
ctx.toast("Run queued", "ok");
|
|
162
|
+
pollRun(run.id || run.run_id);
|
|
163
|
+
} else {
|
|
164
|
+
ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
|
|
165
|
+
}
|
|
161
166
|
|
|
162
167
|
// Refresh runtime so queue/total/recent-runs reflect this run.
|
|
163
168
|
hydrate();
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
async function pollRun(runId) {
|
|
172
|
+
for (let i = 0; i < 80; i += 1) {
|
|
173
|
+
await sleep(i < 10 ? 400 : 1200);
|
|
174
|
+
const res = await ctx.api.agentRunDetail(runId);
|
|
175
|
+
const data = (res && res.data) || {};
|
|
176
|
+
if (!res || !res.ok) return;
|
|
177
|
+
const run = data.run || {};
|
|
178
|
+
logsHost.replaceChildren(renderRunResult(run, run));
|
|
179
|
+
hydrate();
|
|
180
|
+
if (!isActiveStatus(run.status)) {
|
|
181
|
+
const mapped = mapStatus(run.status);
|
|
182
|
+
ctx.toast(`Run ${mapped === "failed" ? "completed with failure" : "finished"}`, mapped === "failed" ? "warn" : "ok");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
166
188
|
/* ── Render a run's result as logs + summary ───────────────────────────── */
|
|
167
189
|
function renderRunResult(run, result) {
|
|
168
190
|
const runId = run.id || run.run_id || result.run_id || result.id;
|
|
169
191
|
const status = mapStatus(result.status || run.status);
|
|
170
|
-
const timeline = Array.isArray(result.timeline) ? result.timeline : [];
|
|
171
|
-
const output = result.output != null ? String(result.output) : "";
|
|
192
|
+
const timeline = Array.isArray(result.timeline) ? result.timeline : (Array.isArray(run.timeline) ? run.timeline : []);
|
|
193
|
+
const output = result.output != null ? String(result.output) : String(run.output_preview || "");
|
|
172
194
|
const retries = Number(result.retries) || 0;
|
|
173
195
|
const active = isActiveStatus(result.status || run.status);
|
|
174
196
|
|
|
@@ -183,9 +205,6 @@ function makeRunConsole(ctx, hosts) {
|
|
|
183
205
|
),
|
|
184
206
|
runId
|
|
185
207
|
? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", {
|
|
186
|
-
// The synchronous runtime finishes inline; Stop is offered but
|
|
187
|
-
// reports honestly (stopped:false + reason) when there's nothing
|
|
188
|
-
// left to interrupt.
|
|
189
208
|
title: active ? "Stop this run" : "This run has already finished",
|
|
190
209
|
on: { click: (e) => stopRun(runId, e.currentTarget) },
|
|
191
210
|
}, c.icon("player-stop"), "Stop")
|
|
@@ -242,7 +261,6 @@ function makeRunConsole(ctx, hosts) {
|
|
|
242
261
|
return;
|
|
243
262
|
}
|
|
244
263
|
if (data.stopped === false || data.stopped == null) {
|
|
245
|
-
// The synchronous runtime cannot interrupt an already-finished run.
|
|
246
264
|
ctx.toast(String(data.reason || "Run already finished — nothing to stop"), "warn");
|
|
247
265
|
} else {
|
|
248
266
|
ctx.toast("Run stopped", "ok");
|
|
@@ -510,16 +528,21 @@ function mapStatus(status) {
|
|
|
510
528
|
const s = String(status || "").toLowerCase();
|
|
511
529
|
if (s === "ok" || s === "retried_ok") return "ready";
|
|
512
530
|
if (s === "failed" || s === "rejected") return "failed";
|
|
513
|
-
if (s === "running" || s === "in_progress") return "active";
|
|
531
|
+
if (s === "running" || s === "in_progress" || s === "queued" || s === "cancelling") return "active";
|
|
532
|
+
if (s === "cancelled" || s === "interrupted") return "warn";
|
|
514
533
|
return s || "idle";
|
|
515
534
|
}
|
|
516
535
|
|
|
517
536
|
// An active run is one that could (in principle) still be stopped.
|
|
518
|
-
const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active"]);
|
|
537
|
+
const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active", "cancelling"]);
|
|
519
538
|
function isActiveStatus(status) {
|
|
520
539
|
return ACTIVE_STATES.has(String(status || "").toLowerCase());
|
|
521
540
|
}
|
|
522
541
|
|
|
542
|
+
function sleep(ms) {
|
|
543
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
544
|
+
}
|
|
545
|
+
|
|
523
546
|
function runNote(r) {
|
|
524
547
|
const out = String(r.output || r.input || "").trim();
|
|
525
548
|
if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* role roster enriched with real run counts, the live recent-runs ledger, and
|
|
5
5
|
* runtime health. Reports unavailable state when the runtime is unreachable.
|
|
6
6
|
* Also drives runs directly: a goal + role selection → POST /agents/api/run →
|
|
7
|
-
* a
|
|
7
|
+
* a durable async run, live logs, final status/output, queue/status, and stop.
|
|
8
8
|
* ========================================================================== */
|
|
9
9
|
|
|
10
10
|
import { timeAgo } from "../core/dom.js";
|
|
@@ -37,7 +37,7 @@ export async function render(ctx) {
|
|
|
37
37
|
h("div",
|
|
38
38
|
h("div.lt3-eyebrow", "Run"),
|
|
39
39
|
h("h3.lt3-panel__title", "Run agents"),
|
|
40
|
-
h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run
|
|
40
|
+
h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run locally with durable progress and cooperative cancellation."),
|
|
41
41
|
),
|
|
42
42
|
runSrc,
|
|
43
43
|
),
|
|
@@ -134,7 +134,7 @@ function makeRunConsole(ctx, hosts) {
|
|
|
134
134
|
if (!roles.length) { ctx.toast("Select at least one role", "info"); return; }
|
|
135
135
|
|
|
136
136
|
runBtn.disabled = true;
|
|
137
|
-
runBtn.replaceChildren(c.icon("loader-2"), "
|
|
137
|
+
runBtn.replaceChildren(c.icon("loader-2"), "Starting…");
|
|
138
138
|
runSrc.replaceChildren(c.sourceBadge("pending"));
|
|
139
139
|
logsHost.replaceChildren(h("div", { style: { "margin-top": "var(--lt3-space-3)" } }, c.loading({ lines: 4 })));
|
|
140
140
|
|
|
@@ -157,18 +157,40 @@ function makeRunConsole(ctx, hosts) {
|
|
|
157
157
|
const run = data.run || {};
|
|
158
158
|
const result = data.result || {};
|
|
159
159
|
logsHost.replaceChildren(renderRunResult(run, result));
|
|
160
|
-
|
|
160
|
+
if (data.accepted && (run.id || run.run_id)) {
|
|
161
|
+
ctx.toast("Run queued", "ok");
|
|
162
|
+
pollRun(run.id || run.run_id);
|
|
163
|
+
} else {
|
|
164
|
+
ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
|
|
165
|
+
}
|
|
161
166
|
|
|
162
167
|
// Refresh runtime so queue/total/recent-runs reflect this run.
|
|
163
168
|
hydrate();
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
async function pollRun(runId) {
|
|
172
|
+
for (let i = 0; i < 80; i += 1) {
|
|
173
|
+
await sleep(i < 10 ? 400 : 1200);
|
|
174
|
+
const res = await ctx.api.agentRunDetail(runId);
|
|
175
|
+
const data = (res && res.data) || {};
|
|
176
|
+
if (!res || !res.ok) return;
|
|
177
|
+
const run = data.run || {};
|
|
178
|
+
logsHost.replaceChildren(renderRunResult(run, run));
|
|
179
|
+
hydrate();
|
|
180
|
+
if (!isActiveStatus(run.status)) {
|
|
181
|
+
const mapped = mapStatus(run.status);
|
|
182
|
+
ctx.toast(`Run ${mapped === "failed" ? "completed with failure" : "finished"}`, mapped === "failed" ? "warn" : "ok");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
166
188
|
/* ── Render a run's result as logs + summary ───────────────────────────── */
|
|
167
189
|
function renderRunResult(run, result) {
|
|
168
190
|
const runId = run.id || run.run_id || result.run_id || result.id;
|
|
169
191
|
const status = mapStatus(result.status || run.status);
|
|
170
|
-
const timeline = Array.isArray(result.timeline) ? result.timeline : [];
|
|
171
|
-
const output = result.output != null ? String(result.output) : "";
|
|
192
|
+
const timeline = Array.isArray(result.timeline) ? result.timeline : (Array.isArray(run.timeline) ? run.timeline : []);
|
|
193
|
+
const output = result.output != null ? String(result.output) : String(run.output_preview || "");
|
|
172
194
|
const retries = Number(result.retries) || 0;
|
|
173
195
|
const active = isActiveStatus(result.status || run.status);
|
|
174
196
|
|
|
@@ -183,9 +205,6 @@ function makeRunConsole(ctx, hosts) {
|
|
|
183
205
|
),
|
|
184
206
|
runId
|
|
185
207
|
? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", {
|
|
186
|
-
// The synchronous runtime finishes inline; Stop is offered but
|
|
187
|
-
// reports honestly (stopped:false + reason) when there's nothing
|
|
188
|
-
// left to interrupt.
|
|
189
208
|
title: active ? "Stop this run" : "This run has already finished",
|
|
190
209
|
on: { click: (e) => stopRun(runId, e.currentTarget) },
|
|
191
210
|
}, c.icon("player-stop"), "Stop")
|
|
@@ -242,7 +261,6 @@ function makeRunConsole(ctx, hosts) {
|
|
|
242
261
|
return;
|
|
243
262
|
}
|
|
244
263
|
if (data.stopped === false || data.stopped == null) {
|
|
245
|
-
// The synchronous runtime cannot interrupt an already-finished run.
|
|
246
264
|
ctx.toast(String(data.reason || "Run already finished — nothing to stop"), "warn");
|
|
247
265
|
} else {
|
|
248
266
|
ctx.toast("Run stopped", "ok");
|
|
@@ -510,16 +528,21 @@ function mapStatus(status) {
|
|
|
510
528
|
const s = String(status || "").toLowerCase();
|
|
511
529
|
if (s === "ok" || s === "retried_ok") return "ready";
|
|
512
530
|
if (s === "failed" || s === "rejected") return "failed";
|
|
513
|
-
if (s === "running" || s === "in_progress") return "active";
|
|
531
|
+
if (s === "running" || s === "in_progress" || s === "queued" || s === "cancelling") return "active";
|
|
532
|
+
if (s === "cancelled" || s === "interrupted") return "warn";
|
|
514
533
|
return s || "idle";
|
|
515
534
|
}
|
|
516
535
|
|
|
517
536
|
// An active run is one that could (in principle) still be stopped.
|
|
518
|
-
const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active"]);
|
|
537
|
+
const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active", "cancelling"]);
|
|
519
538
|
function isActiveStatus(status) {
|
|
520
539
|
return ACTIVE_STATES.has(String(status || "").toLowerCase());
|
|
521
540
|
}
|
|
522
541
|
|
|
542
|
+
function sleep(ms) {
|
|
543
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
544
|
+
}
|
|
545
|
+
|
|
523
546
|
function runNote(r) {
|
|
524
547
|
const out = String(r.output || r.input || "").trim();
|
|
525
548
|
if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* ========================================================================== */
|
|
13
13
|
|
|
14
14
|
import { timeAgo } from "../core/dom.a2773eb0.js";
|
|
15
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
15
16
|
|
|
16
17
|
export const layout = "flush";
|
|
17
18
|
|
|
@@ -520,10 +521,32 @@ export async function render(ctx) {
|
|
|
520
521
|
? fileRefs.slice(0, 6).map((p) => ctxItem("var(--faint)", p, null))
|
|
521
522
|
: [ctxEmpty("No file references yet")]),
|
|
522
523
|
|
|
524
|
+
ctxSection(t("chat.whyContext"), "route",
|
|
525
|
+
traceItems(state.lastTrace, q)),
|
|
526
|
+
|
|
523
527
|
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm.lt3-btn--block", { on: { click: () => navigate("hybrid-search", q ? { q } : undefined) } }, icon("arrows-join"), "Open Hybrid Search"),
|
|
524
528
|
);
|
|
525
529
|
}
|
|
526
530
|
|
|
531
|
+
function traceItems(trace, query) {
|
|
532
|
+
if (!trace) return [ctxEmpty(t("chat.traceUnavailable"))];
|
|
533
|
+
const items = [];
|
|
534
|
+
const meta = trace.retrieval_metadata || {};
|
|
535
|
+
items.push(ctxItem("var(--lt3-pillar-hybrid)", t("chat.traceQuestion"), trace.question || query || "—"));
|
|
536
|
+
if (trace.confidence != null) items.push(ctxItem("var(--lt3-pillar-vector)", t("chat.traceConfidence"), Number(trace.confidence).toFixed(2)));
|
|
537
|
+
if (meta.mode || meta.strategy) items.push(ctxItem("var(--lt3-pillar-hybrid)", "retrieval", meta.mode || meta.strategy));
|
|
538
|
+
if (Array.isArray(trace.graph_nodes) && trace.graph_nodes.length) {
|
|
539
|
+
items.push(ctxItem("var(--lt3-pillar-graph)", t("chat.traceReasons"), `${trace.graph_nodes.length} graph node(s)`));
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(trace.vector_matches) && trace.vector_matches.length) {
|
|
542
|
+
items.push(ctxItem("var(--lt3-pillar-vector)", "vector", `${trace.vector_matches.length} match(es)`));
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(trace.source_files) && trace.source_files.length) {
|
|
545
|
+
items.push(ctxItem("var(--faint)", "files", `${trace.source_files.length} source file(s)`));
|
|
546
|
+
}
|
|
547
|
+
return items.length ? items : [ctxEmpty(t("chat.traceUnavailable"))];
|
|
548
|
+
}
|
|
549
|
+
|
|
527
550
|
function ctxSection(title, icn, children) {
|
|
528
551
|
return h("section",
|
|
529
552
|
h("div.lt3-ctx-sec__title", icon(icn), title),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* ========================================================================== */
|
|
13
13
|
|
|
14
14
|
import { timeAgo } from "../core/dom.js";
|
|
15
|
+
import { t } from "../core/i18n.js";
|
|
15
16
|
|
|
16
17
|
export const layout = "flush";
|
|
17
18
|
|
|
@@ -520,10 +521,32 @@ export async function render(ctx) {
|
|
|
520
521
|
? fileRefs.slice(0, 6).map((p) => ctxItem("var(--faint)", p, null))
|
|
521
522
|
: [ctxEmpty("No file references yet")]),
|
|
522
523
|
|
|
524
|
+
ctxSection(t("chat.whyContext"), "route",
|
|
525
|
+
traceItems(state.lastTrace, q)),
|
|
526
|
+
|
|
523
527
|
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm.lt3-btn--block", { on: { click: () => navigate("hybrid-search", q ? { q } : undefined) } }, icon("arrows-join"), "Open Hybrid Search"),
|
|
524
528
|
);
|
|
525
529
|
}
|
|
526
530
|
|
|
531
|
+
function traceItems(trace, query) {
|
|
532
|
+
if (!trace) return [ctxEmpty(t("chat.traceUnavailable"))];
|
|
533
|
+
const items = [];
|
|
534
|
+
const meta = trace.retrieval_metadata || {};
|
|
535
|
+
items.push(ctxItem("var(--lt3-pillar-hybrid)", t("chat.traceQuestion"), trace.question || query || "—"));
|
|
536
|
+
if (trace.confidence != null) items.push(ctxItem("var(--lt3-pillar-vector)", t("chat.traceConfidence"), Number(trace.confidence).toFixed(2)));
|
|
537
|
+
if (meta.mode || meta.strategy) items.push(ctxItem("var(--lt3-pillar-hybrid)", "retrieval", meta.mode || meta.strategy));
|
|
538
|
+
if (Array.isArray(trace.graph_nodes) && trace.graph_nodes.length) {
|
|
539
|
+
items.push(ctxItem("var(--lt3-pillar-graph)", t("chat.traceReasons"), `${trace.graph_nodes.length} graph node(s)`));
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(trace.vector_matches) && trace.vector_matches.length) {
|
|
542
|
+
items.push(ctxItem("var(--lt3-pillar-vector)", "vector", `${trace.vector_matches.length} match(es)`));
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(trace.source_files) && trace.source_files.length) {
|
|
545
|
+
items.push(ctxItem("var(--faint)", "files", `${trace.source_files.length} source file(s)`));
|
|
546
|
+
}
|
|
547
|
+
return items.length ? items : [ctxEmpty(t("chat.traceUnavailable"))];
|
|
548
|
+
}
|
|
549
|
+
|
|
527
550
|
function ctxSection(title, icn, children) {
|
|
528
551
|
return h("section",
|
|
529
552
|
h("div.lt3-ctx-sec__title", icon(icn), title),
|