negotium 0.1.7 → 0.1.9
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 +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-bloat alert.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from the Telegram build's `telegram/topic/alert.ts`. After each AI turn
|
|
5
|
+
* we inspect the provider's latest-request context occupancy. Aggregate turn
|
|
6
|
+
* input is deliberately not used: a tool-heavy turn can make many cached model
|
|
7
|
+
* calls and report millions of billable input tokens while the actual context
|
|
8
|
+
* window remains mostly empty.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately quiet: at most ONE alert per session lifetime (until the next
|
|
11
|
+
* `/new` clears it), so a long-running topic is never nagged turn after turn.
|
|
12
|
+
*
|
|
13
|
+
* The heavy lifting here (threshold, per-topic dedup, message text) is kept free
|
|
14
|
+
* of I/O so it unit-tests cleanly; the caller in `runtime/turn-runner.ts` handles
|
|
15
|
+
* the General/subagent guards and the actual posting.
|
|
16
|
+
*/
|
|
17
|
+
import type { TokenUsage } from "#types";
|
|
18
|
+
|
|
19
|
+
const CONTEXT_ALERT_RATIO = 0.8;
|
|
20
|
+
|
|
21
|
+
// Topics already warned this session, keyed by (user, topic) so the same topic
|
|
22
|
+
// under different users tracks independently. Cleared on session reset (`/new`).
|
|
23
|
+
const alerted = new Set<string>();
|
|
24
|
+
|
|
25
|
+
const alertKey = (userId: string, topicId: string) => `${userId}:${topicId}`;
|
|
26
|
+
|
|
27
|
+
/** Latest-request context occupancy, or null when the provider did not report it. */
|
|
28
|
+
export function contextUsageRatio(usage: TokenUsage): number | null {
|
|
29
|
+
if (
|
|
30
|
+
usage.contextTokens === undefined ||
|
|
31
|
+
usage.contextWindow === undefined ||
|
|
32
|
+
!Number.isFinite(usage.contextTokens) ||
|
|
33
|
+
!Number.isFinite(usage.contextWindow) ||
|
|
34
|
+
usage.contextTokens < 0 ||
|
|
35
|
+
usage.contextWindow <= 0
|
|
36
|
+
) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return usage.contextTokens / usage.contextWindow;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fmt(n: number): string {
|
|
43
|
+
return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}K`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildUsageAlertText(topicTitle: string, usage: TokenUsage): string {
|
|
47
|
+
const ratio = contextUsageRatio(usage) ?? 0;
|
|
48
|
+
const used = usage.contextTokens ?? 0;
|
|
49
|
+
const window = usage.contextWindow ?? 0;
|
|
50
|
+
return (
|
|
51
|
+
`⚠️ "${topicTitle}" context가 ${Math.round(ratio * 100)}% 찼어요 ` +
|
|
52
|
+
`(${fmt(used)} / ${fmt(window)} 토큰)\n\n` +
|
|
53
|
+
`이 토픽에서 /compact 를 입력하면 핵심 맥락을 요약해 이어가면서 context를 줄입니다. ` +
|
|
54
|
+
`완전히 새로 시작하려면 /new 를 사용하세요.\n\n` +
|
|
55
|
+
`두 명령 모두 지금까지 주고받은 보이는 대화 내역은 그대로 유지합니다.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* If the latest request filled at least 80% of the provider context window and
|
|
61
|
+
* the topic hasn't been warned yet this session, return the alert text and mark
|
|
62
|
+
* it warned; otherwise null. Fires at most once per session lifetime — the
|
|
63
|
+
* notice won't repeat turn after turn — and re-arms only after `/new` clears it.
|
|
64
|
+
*/
|
|
65
|
+
export function nextUsageAlert(
|
|
66
|
+
userId: string,
|
|
67
|
+
topicId: string,
|
|
68
|
+
topicTitle: string,
|
|
69
|
+
usage: TokenUsage,
|
|
70
|
+
): string | null {
|
|
71
|
+
const ratio = contextUsageRatio(usage);
|
|
72
|
+
if (ratio === null || ratio < CONTEXT_ALERT_RATIO) return null;
|
|
73
|
+
|
|
74
|
+
const key = alertKey(userId, topicId);
|
|
75
|
+
if (alerted.has(key)) return null;
|
|
76
|
+
alerted.add(key);
|
|
77
|
+
|
|
78
|
+
return buildUsageAlertText(topicTitle, usage);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Re-arm the alert for a topic so a fresh session can warn again (called on `/new`). */
|
|
82
|
+
export function clearQueryUsageAlert(userId: string, topicId: string): void {
|
|
83
|
+
alerted.delete(alertKey(userId, topicId));
|
|
84
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const MERMAID_BROWSER_VERSION = "11.4.1";
|
|
2
|
+
export const MERMAID_BROWSER_ASSET_PATH = `/api/v1/assets/mermaid-${MERMAID_BROWSER_VERSION}.min.js`;
|
|
3
|
+
export const MERMAID_BROWSER_ASSET_RELATIVE_URL = `../../../../assets/mermaid-${MERMAID_BROWSER_VERSION}.min.js`;
|
|
4
|
+
|
|
5
|
+
export type MermaidTheme = "default" | "neutral" | "dark" | "forest";
|
|
6
|
+
|
|
7
|
+
function escapeHtml(value: string): string {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """)
|
|
13
|
+
.replace(/'/g, "'");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeMermaidTheme(value: unknown): MermaidTheme {
|
|
17
|
+
return value === "default" || value === "dark" || value === "forest" ? value : "neutral";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Preserve the selected theme when a stored Mermaid document is rebuilt. */
|
|
21
|
+
export function mermaidThemeFromHtml(html: string): MermaidTheme {
|
|
22
|
+
const match = html.match(/\btheme\s*:\s*["'](default|neutral|dark|forest)["']/i);
|
|
23
|
+
return normalizeMermaidTheme(match?.[1]?.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a self-contained Mermaid viewer. Diagrams retain a readable minimum
|
|
28
|
+
* scale and scroll instead of being squeezed into a narrow side panel; quiet
|
|
29
|
+
* zoom controls remain available for large graphs.
|
|
30
|
+
*/
|
|
31
|
+
export function buildMermaidHtml(
|
|
32
|
+
code: string,
|
|
33
|
+
theme: MermaidTheme,
|
|
34
|
+
scriptUrl = MERMAID_BROWSER_ASSET_RELATIVE_URL,
|
|
35
|
+
): string {
|
|
36
|
+
const safeCode = escapeHtml(code);
|
|
37
|
+
const safeTheme = JSON.stringify(theme);
|
|
38
|
+
const safeScriptUrl = escapeHtml(scriptUrl);
|
|
39
|
+
return `<!doctype html>
|
|
40
|
+
<html data-otium-mermaid-version="3">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="utf-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
44
|
+
<style>
|
|
45
|
+
:root{color-scheme:light;--ink:#161614;--ivory:#F6F2EA;--surface:#FFFCF6;--graphite:#2C2D2A;--celadon:#A8BDB2;--border:#DDD7CD}
|
|
46
|
+
*{box-sizing:border-box}
|
|
47
|
+
html,body{margin:0;width:100%;min-height:100%;background:var(--ivory);color:var(--ink);font-family:Geist,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
48
|
+
body{overflow:hidden}
|
|
49
|
+
.viewport{width:100vw;height:100vh;min-height:260px;overflow:auto;padding:58px 24px 24px;scrollbar-gutter:stable}
|
|
50
|
+
.mermaid{display:flex;width:max-content;min-width:100%;justify-content:center;align-items:flex-start}
|
|
51
|
+
.mermaid svg{display:block;max-width:none!important;height:auto!important;flex:none}
|
|
52
|
+
.controls{position:fixed;z-index:2;top:12px;right:12px;display:flex;align-items:center;gap:2px;padding:3px;background:color-mix(in srgb,var(--surface) 94%,transparent);border:1px solid var(--border);border-radius:6px}
|
|
53
|
+
.controls button{height:28px;min-width:30px;padding:0 8px;border:0;border-radius:4px;background:transparent;color:var(--graphite);font:600 12px/1 Geist,system-ui,sans-serif;cursor:pointer}
|
|
54
|
+
.controls button:hover{background:color-mix(in srgb,var(--celadon) 24%,transparent)}
|
|
55
|
+
.controls button:focus-visible{outline:2px solid var(--celadon);outline-offset:1px}
|
|
56
|
+
.zoom-value{min-width:46px;color:var(--graphite);font:500 11px/1 Geist,system-ui,sans-serif;text-align:center;font-variant-numeric:tabular-nums}
|
|
57
|
+
.error{margin:0;white-space:pre-wrap;color:#7D2E2E;background:#FFF5F2;border:1px solid #E4B9B1;border-radius:6px;padding:14px;font:13px ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
58
|
+
@media(max-width:600px){.viewport{padding:54px 14px 18px}.controls{top:10px;right:10px}}
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<div class="controls" role="group" aria-label="Diagram zoom">
|
|
63
|
+
<button type="button" data-zoom-out aria-label="Zoom out">−</button>
|
|
64
|
+
<span class="zoom-value" aria-live="polite">100%</span>
|
|
65
|
+
<button type="button" data-zoom-in aria-label="Zoom in">+</button>
|
|
66
|
+
<button type="button" data-zoom-fit aria-label="Fit diagram">Fit</button>
|
|
67
|
+
</div>
|
|
68
|
+
<main class="viewport"><pre class="mermaid">${safeCode}</pre></main>
|
|
69
|
+
<script data-otium-mermaid-runtime src="${safeScriptUrl}"></script>
|
|
70
|
+
<script>
|
|
71
|
+
(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const runtime = globalThis.mermaid;
|
|
74
|
+
if (!runtime) throw new Error("Mermaid renderer failed to load.");
|
|
75
|
+
runtime.initialize({ startOnLoad: false, securityLevel: "strict", theme: ${safeTheme} });
|
|
76
|
+
await runtime.run({ querySelector: ".mermaid" });
|
|
77
|
+
const viewport = document.querySelector(".viewport");
|
|
78
|
+
const svg = document.querySelector(".mermaid svg");
|
|
79
|
+
const value = document.querySelector(".zoom-value");
|
|
80
|
+
const bounds = svg && svg.viewBox && svg.viewBox.baseVal;
|
|
81
|
+
const naturalWidth = Math.max(1, Math.ceil((bounds && bounds.width) || (svg && svg.getBoundingClientRect().width) || 1));
|
|
82
|
+
let scale = Math.max(0.72, Math.min(1, (viewport.clientWidth - 48) / naturalWidth));
|
|
83
|
+
const applyScale = (next, center = true) => {
|
|
84
|
+
const previousWidth = naturalWidth * scale;
|
|
85
|
+
const centerRatio = previousWidth > viewport.clientWidth
|
|
86
|
+
? (viewport.scrollLeft + viewport.clientWidth / 2) / previousWidth
|
|
87
|
+
: 0.5;
|
|
88
|
+
scale = Math.max(0.5, Math.min(2, next));
|
|
89
|
+
svg.style.width = Math.round(naturalWidth * scale) + "px";
|
|
90
|
+
svg.style.maxWidth = "none";
|
|
91
|
+
svg.style.height = "auto";
|
|
92
|
+
value.textContent = Math.round(scale * 100) + "%";
|
|
93
|
+
if (center) requestAnimationFrame(() => {
|
|
94
|
+
viewport.scrollLeft = Math.max(0, centerRatio * viewport.scrollWidth - viewport.clientWidth / 2);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
document.querySelector("[data-zoom-out]").addEventListener("click", () => applyScale(scale - 0.1));
|
|
98
|
+
document.querySelector("[data-zoom-in]").addEventListener("click", () => applyScale(scale + 0.1));
|
|
99
|
+
document.querySelector("[data-zoom-fit]").addEventListener("click", () => applyScale(Math.min(1, (viewport.clientWidth - 48) / naturalWidth)));
|
|
100
|
+
applyScale(scale, false);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
document.querySelector(".controls")?.remove();
|
|
103
|
+
document.querySelector(".viewport").innerHTML = '<pre class="error">' + String(error && error.message ? error.message : error).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])) + '</pre>';
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>`;
|
|
109
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/** Visual (HTML snippet) storage for the Otium WebView renderer.
|
|
2
|
+
*
|
|
3
|
+
* Each topic stores up to 10 visuals (newest-first); older ones are pruned.
|
|
4
|
+
* Each user's active selection is tracked separately so multiple people in
|
|
5
|
+
* the same topic can be on different visuals independently.
|
|
6
|
+
*
|
|
7
|
+
* Port of the storage/prompt-context half of otium's `api/routes/visual.ts`;
|
|
8
|
+
* the REST routes (serve HTML/media, CSP headers, list/select endpoints)
|
|
9
|
+
* stayed in the host.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { db } from "#storage/forum-db";
|
|
13
|
+
|
|
14
|
+
const MAX_HISTORY = 10;
|
|
15
|
+
const MAX_VISUAL_TITLE_CHARS = 180;
|
|
16
|
+
export const VISUAL_MEDIA_URL_PLACEHOLDER = "__NEGOTIUM_VISUAL_MEDIA_URL__";
|
|
17
|
+
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS api_topic_visuals (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
topic_id TEXT NOT NULL,
|
|
22
|
+
html TEXT NOT NULL,
|
|
23
|
+
title TEXT,
|
|
24
|
+
created_at INTEGER NOT NULL,
|
|
25
|
+
kind TEXT NOT NULL DEFAULT 'html',
|
|
26
|
+
source TEXT,
|
|
27
|
+
file_id TEXT,
|
|
28
|
+
mime_type TEXT,
|
|
29
|
+
media_token TEXT
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_api_topic_visuals_topic
|
|
32
|
+
ON api_topic_visuals(topic_id, created_at DESC);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS api_topic_visual_views (
|
|
35
|
+
topic_id TEXT NOT NULL,
|
|
36
|
+
user_id TEXT NOT NULL,
|
|
37
|
+
visual_id INTEGER NOT NULL,
|
|
38
|
+
PRIMARY KEY (topic_id, user_id)
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
for (const statement of [
|
|
43
|
+
"ALTER TABLE api_topic_visuals ADD COLUMN kind TEXT NOT NULL DEFAULT 'html'",
|
|
44
|
+
"ALTER TABLE api_topic_visuals ADD COLUMN source TEXT",
|
|
45
|
+
"ALTER TABLE api_topic_visuals ADD COLUMN file_id TEXT",
|
|
46
|
+
"ALTER TABLE api_topic_visuals ADD COLUMN mime_type TEXT",
|
|
47
|
+
"ALTER TABLE api_topic_visuals ADD COLUMN media_token TEXT",
|
|
48
|
+
]) {
|
|
49
|
+
try {
|
|
50
|
+
db.exec(statement);
|
|
51
|
+
} catch {
|
|
52
|
+
// Column already exists.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── DB helpers ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export const VISUAL_KINDS = ["html", "mermaid", "image", "video"] as const;
|
|
59
|
+
export type VisualKind = (typeof VISUAL_KINDS)[number];
|
|
60
|
+
|
|
61
|
+
type VisualRow = {
|
|
62
|
+
id: number;
|
|
63
|
+
topic_id: string;
|
|
64
|
+
html: string;
|
|
65
|
+
title: string | null;
|
|
66
|
+
created_at: number;
|
|
67
|
+
kind: VisualKind;
|
|
68
|
+
source: string | null;
|
|
69
|
+
file_id: string | null;
|
|
70
|
+
mime_type: string | null;
|
|
71
|
+
media_token: string | null;
|
|
72
|
+
};
|
|
73
|
+
type VisualListRow = {
|
|
74
|
+
id: number;
|
|
75
|
+
title: string | null;
|
|
76
|
+
created_at: number;
|
|
77
|
+
kind: VisualKind;
|
|
78
|
+
mime_type: string | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function normalizeVisualTitle(value: unknown): string | undefined {
|
|
82
|
+
if (typeof value !== "string") return undefined;
|
|
83
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
84
|
+
if (!normalized) return undefined;
|
|
85
|
+
return normalized.length > MAX_VISUAL_TITLE_CHARS
|
|
86
|
+
? `${normalized.slice(0, MAX_VISUAL_TITLE_CHARS).trimEnd()}...`
|
|
87
|
+
: normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeVisualKind(value: unknown): VisualKind {
|
|
91
|
+
return VISUAL_KINDS.includes(value as VisualKind) ? (value as VisualKind) : "html";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function insertVisual(input: {
|
|
95
|
+
topicId: string;
|
|
96
|
+
html: string;
|
|
97
|
+
title: string | null;
|
|
98
|
+
kind?: VisualKind;
|
|
99
|
+
source?: string | null;
|
|
100
|
+
fileId?: string | null;
|
|
101
|
+
mimeType?: string | null;
|
|
102
|
+
mediaToken?: string | null;
|
|
103
|
+
}): number {
|
|
104
|
+
const result = db
|
|
105
|
+
.query<
|
|
106
|
+
{ id: number },
|
|
107
|
+
[
|
|
108
|
+
string,
|
|
109
|
+
string,
|
|
110
|
+
string | null,
|
|
111
|
+
number,
|
|
112
|
+
string,
|
|
113
|
+
string | null,
|
|
114
|
+
string | null,
|
|
115
|
+
string | null,
|
|
116
|
+
string | null,
|
|
117
|
+
]
|
|
118
|
+
>(
|
|
119
|
+
`INSERT INTO api_topic_visuals
|
|
120
|
+
(topic_id, html, title, created_at, kind, source, file_id, mime_type, media_token)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
122
|
+
RETURNING id`,
|
|
123
|
+
)
|
|
124
|
+
.get(
|
|
125
|
+
input.topicId,
|
|
126
|
+
input.html,
|
|
127
|
+
input.title,
|
|
128
|
+
Date.now(),
|
|
129
|
+
input.kind ?? "html",
|
|
130
|
+
input.source ?? null,
|
|
131
|
+
input.fileId ?? null,
|
|
132
|
+
input.mimeType ?? null,
|
|
133
|
+
input.mediaToken ?? null,
|
|
134
|
+
);
|
|
135
|
+
if (!result) throw new Error("Failed to insert visual");
|
|
136
|
+
// Prune: keep only the newest MAX_HISTORY rows per topic.
|
|
137
|
+
db.query(
|
|
138
|
+
`DELETE FROM api_topic_visual_views
|
|
139
|
+
WHERE visual_id IN (
|
|
140
|
+
SELECT id FROM api_topic_visuals
|
|
141
|
+
WHERE topic_id = ? AND id NOT IN (
|
|
142
|
+
SELECT id FROM api_topic_visuals WHERE topic_id = ?
|
|
143
|
+
ORDER BY created_at DESC LIMIT ?
|
|
144
|
+
)
|
|
145
|
+
)`,
|
|
146
|
+
).run(input.topicId, input.topicId, MAX_HISTORY);
|
|
147
|
+
db.query(
|
|
148
|
+
`DELETE FROM api_topic_visuals
|
|
149
|
+
WHERE topic_id = ? AND id NOT IN (
|
|
150
|
+
SELECT id FROM api_topic_visuals WHERE topic_id = ?
|
|
151
|
+
ORDER BY created_at DESC LIMIT ?
|
|
152
|
+
)`,
|
|
153
|
+
).run(input.topicId, input.topicId, MAX_HISTORY);
|
|
154
|
+
return result.id;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function listVisuals(topicId: string): VisualListRow[] {
|
|
158
|
+
return db
|
|
159
|
+
.query<Omit<VisualListRow, "kind"> & { kind: string | null }, [string]>(
|
|
160
|
+
"SELECT id, title, created_at, kind, mime_type FROM api_topic_visuals WHERE topic_id = ? ORDER BY created_at DESC",
|
|
161
|
+
)
|
|
162
|
+
.all(topicId)
|
|
163
|
+
.map((row) => ({ ...row, kind: normalizeVisualKind(row.kind) }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getVisualById(id: number): VisualRow | null {
|
|
167
|
+
const row = db
|
|
168
|
+
.query<Omit<VisualRow, "kind"> & { kind: string | null }, [number]>(
|
|
169
|
+
"SELECT * FROM api_topic_visuals WHERE id = ?",
|
|
170
|
+
)
|
|
171
|
+
.get(id);
|
|
172
|
+
return row ? { ...row, kind: normalizeVisualKind(row.kind) } : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function topicHasVisualFileId(topicId: string, fileId: string): boolean {
|
|
176
|
+
const row = db
|
|
177
|
+
.query<{ id: number }, [string, string]>(
|
|
178
|
+
"SELECT id FROM api_topic_visuals WHERE topic_id = ? AND file_id = ? LIMIT 1",
|
|
179
|
+
)
|
|
180
|
+
.get(topicId, fileId);
|
|
181
|
+
return Boolean(row);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getUserActiveVisualId(topicId: string, userId: string): number | null {
|
|
185
|
+
const row = db
|
|
186
|
+
.query<{ visual_id: number }, [string, string]>(
|
|
187
|
+
"SELECT visual_id FROM api_topic_visual_views WHERE topic_id = ? AND user_id = ?",
|
|
188
|
+
)
|
|
189
|
+
.get(topicId, userId);
|
|
190
|
+
return row?.visual_id ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function setUserActiveVisualId(topicId: string, userId: string, visualId: number): void {
|
|
194
|
+
db.query(
|
|
195
|
+
`INSERT INTO api_topic_visual_views (topic_id, user_id, visual_id) VALUES (?, ?, ?)
|
|
196
|
+
ON CONFLICT(topic_id, user_id) DO UPDATE SET visual_id = excluded.visual_id`,
|
|
197
|
+
).run(topicId, userId, visualId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Public helpers ──────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
// Dark-step token values shared by the two dark selectors below (OS-auto via
|
|
203
|
+
// media query, and the explicit data-viz-theme="dark" override). Single source
|
|
204
|
+
// so the manual switch can never drift from the auto dark palette.
|
|
205
|
+
const VIZ_DARK_TOKENS =
|
|
206
|
+
"--viz-surface:#1a1a1a;--viz-plane:#111111;--viz-text:#f2f2f0;--viz-text-secondary:#b6b8ae;--viz-muted:#8f938a;--viz-grid:#2a2a2a;--viz-axis:#3a3a38;--viz-border:rgba(255,255,255,.12);" +
|
|
207
|
+
"--viz-accent:#98ac83;--viz-accent-strong:#b3c49f;--viz-accent-soft:#2a3320;" +
|
|
208
|
+
"--viz-series-1:#98ac83;--viz-series-2:#9aa08f;--viz-series-3:#b0b4a8;--viz-series-4:#7f8f70;--viz-series-5:#d9dbcf;--viz-series-6:#c9d1bf;" +
|
|
209
|
+
"--viz-seq-100:#2f3a26;--viz-seq-200:#40513b;--viz-seq-300:#556845;--viz-seq-400:#6c7d57;--viz-seq-500:#8a9a74;--viz-seq-600:#a7b499;--viz-seq-700:#c9d1bf;" +
|
|
210
|
+
"--viz-good:#4caf50;--viz-warning:#d9a520;--viz-serious:#d98a5a;--viz-critical:#e06666;";
|
|
211
|
+
|
|
212
|
+
// Base design tokens injected into every stored HTML visual so charts share one
|
|
213
|
+
// clean, near-white look built around the olive accent #40513B, with a selected
|
|
214
|
+
// dark-mode step (values chosen for the dark surface, validated for contrast and
|
|
215
|
+
// CVD, not an automatic flip). Placed first in <head> so an agent's own styles
|
|
216
|
+
// override the reset, while the --viz-* custom properties stay available to
|
|
217
|
+
// reference. Values mirror prompts/sessions/visual-design.md; keep them in sync.
|
|
218
|
+
//
|
|
219
|
+
// Theme resolution: no data-viz-theme attribute → follow the OS
|
|
220
|
+
// (prefers-color-scheme); data-viz-theme="light"/"dark" → forced by the in-viz
|
|
221
|
+
// switch (see VISUAL_THEME_TOGGLE below).
|
|
222
|
+
const VISUAL_BASE_STYLE = `<style id="viz-base">
|
|
223
|
+
:root{color-scheme:light dark;
|
|
224
|
+
--viz-surface:#ffffff;--viz-plane:#f6f6f4;--viz-text:#141414;--viz-text-secondary:#565b50;--viz-muted:#8a8f84;--viz-grid:#ececea;--viz-axis:#d7d7d2;--viz-border:rgba(20,20,20,.10);
|
|
225
|
+
--viz-accent:#40513b;--viz-accent-strong:#2c3826;--viz-accent-soft:#eef1ea;
|
|
226
|
+
--viz-series-1:#40513b;--viz-series-2:#8c9187;--viz-series-3:#6b7a5f;--viz-series-4:#b9bcb4;--viz-series-5:#2c3826;--viz-series-6:#a0a89a;
|
|
227
|
+
--viz-seq-100:#eef1ea;--viz-seq-200:#d4dcca;--viz-seq-300:#9aab8c;--viz-seq-400:#77896a;--viz-seq-500:#566848;--viz-seq-600:#40513b;--viz-seq-700:#2c3826;
|
|
228
|
+
--viz-good:#2e7d32;--viz-warning:#b8860b;--viz-serious:#bf6a3a;--viz-critical:#b23b3b;}
|
|
229
|
+
@media (prefers-color-scheme:dark){:root:not([data-viz-theme=light]){${VIZ_DARK_TOKENS}}}
|
|
230
|
+
:root[data-viz-theme=dark]{${VIZ_DARK_TOKENS}}
|
|
231
|
+
:root[data-viz-theme=light]{color-scheme:light}
|
|
232
|
+
:root[data-viz-theme=dark]{color-scheme:dark}
|
|
233
|
+
*{box-sizing:border-box}
|
|
234
|
+
html{background:var(--viz-surface)}
|
|
235
|
+
html,body{margin:0;min-height:100%}
|
|
236
|
+
body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--viz-surface);color:var(--viz-text);-webkit-text-size-adjust:100%}
|
|
237
|
+
</style>`;
|
|
238
|
+
|
|
239
|
+
// Floating light/dark switch rendered inside every HTML visual. One inline
|
|
240
|
+
// <script> (CSP already allows 'unsafe-inline' for html visuals) that injects
|
|
241
|
+
// its own style, so upgrades and idempotency hinge on a single id. The button
|
|
242
|
+
// shows the mode a click switches TO (moon while light, sun while dark) and
|
|
243
|
+
// flips the data-viz-theme override consumed by VISUAL_BASE_STYLE above.
|
|
244
|
+
// No persistence: sandboxed srcDoc iframes have no reliable localStorage, and
|
|
245
|
+
// per-document lifetime matches how visuals are reopened from the store.
|
|
246
|
+
const VISUAL_THEME_TOGGLE = `<script id="viz-theme-toggle">
|
|
247
|
+
(function(){function init(){if(document.getElementById("viz-theme-toggle-btn"))return;
|
|
248
|
+
var s=document.createElement("style");
|
|
249
|
+
s.textContent="#viz-theme-toggle-btn{position:fixed;top:10px;right:10px;z-index:2147483647;width:30px;height:30px;border-radius:50%;border:1px solid var(--viz-border);background:var(--viz-plane);font:15px/1 system-ui;display:grid;place-items:center;cursor:pointer;opacity:.5;transition:opacity .15s;padding:0}#viz-theme-toggle-btn:hover{opacity:1}";
|
|
250
|
+
document.head.appendChild(s);
|
|
251
|
+
var b=document.createElement("button");b.id="viz-theme-toggle-btn";b.type="button";b.setAttribute("aria-label","Toggle color theme");
|
|
252
|
+
var mq=window.matchMedia?window.matchMedia("(prefers-color-scheme: dark)"):null;
|
|
253
|
+
function effective(){var f=document.documentElement.getAttribute("data-viz-theme");return f==="dark"||f==="light"?f:(mq&&mq.matches?"dark":"light")}
|
|
254
|
+
function paint(){var dark=effective()==="dark";b.textContent=dark?"\\u2600\\uFE0F":"\\uD83C\\uDF19";b.title=dark?"Light mode":"Dark mode"}
|
|
255
|
+
b.addEventListener("click",function(){document.documentElement.setAttribute("data-viz-theme",effective()==="dark"?"light":"dark");paint()});
|
|
256
|
+
if(mq&&mq.addEventListener)mq.addEventListener("change",paint);
|
|
257
|
+
paint();document.body.appendChild(b)}
|
|
258
|
+
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",init);else init()})();
|
|
259
|
+
</script>`;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Bake the base design tokens + theme switch into an agent-provided HTML
|
|
263
|
+
* document so every visual shares one palette/reset regardless of render path
|
|
264
|
+
* (web srcDoc, desktop iframe, and native WebView all serve this stored HTML
|
|
265
|
+
* directly). Inserted at the top of <head> so agent styles win the cascade;
|
|
266
|
+
* the --viz-* variables are only added, never overriding what the agent set.
|
|
267
|
+
* Idempotent AND upgrading: a doc that already carries a baked block gets it
|
|
268
|
+
* swapped for the current version (so legacy stored visuals pick up the theme
|
|
269
|
+
* switch at serve time) instead of stacking a second copy.
|
|
270
|
+
*/
|
|
271
|
+
export function withVisualBaseStyle(html: string): string {
|
|
272
|
+
const inject = `${VISUAL_BASE_STYLE}${VISUAL_THEME_TOGGLE}`;
|
|
273
|
+
if (html.includes('id="viz-base"')) {
|
|
274
|
+
let out = html.replace(/<style id="viz-base">[\s\S]*?<\/style>/, VISUAL_BASE_STYLE);
|
|
275
|
+
out = out.includes('id="viz-theme-toggle"')
|
|
276
|
+
? out.replace(/<script id="viz-theme-toggle">[\s\S]*?<\/script>/, VISUAL_THEME_TOGGLE)
|
|
277
|
+
: out.replace(VISUAL_BASE_STYLE, inject);
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
if (/<head(?:\s[^>]*)?>/i.test(html)) {
|
|
281
|
+
return html.replace(/<head(?:\s[^>]*)?>/i, (match) => `${match}${inject}`);
|
|
282
|
+
}
|
|
283
|
+
if (/<html(?:\s[^>]*)?>/i.test(html)) {
|
|
284
|
+
return html.replace(/<html(?:\s[^>]*)?>/i, (match) => `${match}<head>${inject}</head>`);
|
|
285
|
+
}
|
|
286
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${inject}</head><body>${html}</body></html>`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Persist a new visual and return its id. Called by the show_html tool path. */
|
|
290
|
+
export function storeTopicVisual(
|
|
291
|
+
topicId: string,
|
|
292
|
+
html: string,
|
|
293
|
+
title?: string,
|
|
294
|
+
activeUserId?: string,
|
|
295
|
+
): number {
|
|
296
|
+
const visualId = insertVisual({
|
|
297
|
+
topicId,
|
|
298
|
+
// Rendered document carries the house style; source stays the agent's own
|
|
299
|
+
// HTML so edit-context feedback isn't padded with our boilerplate.
|
|
300
|
+
html: withVisualBaseStyle(html),
|
|
301
|
+
title: normalizeVisualTitle(title) ?? null,
|
|
302
|
+
kind: "html",
|
|
303
|
+
source: html,
|
|
304
|
+
});
|
|
305
|
+
if (activeUserId) setUserActiveVisualId(topicId, activeUserId, visualId);
|
|
306
|
+
return visualId;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function storeTopicMermaidVisual(
|
|
310
|
+
topicId: string,
|
|
311
|
+
code: string,
|
|
312
|
+
html: string,
|
|
313
|
+
title?: string,
|
|
314
|
+
activeUserId?: string,
|
|
315
|
+
): number {
|
|
316
|
+
const visualId = insertVisual({
|
|
317
|
+
topicId,
|
|
318
|
+
html,
|
|
319
|
+
title: normalizeVisualTitle(title) ?? null,
|
|
320
|
+
kind: "mermaid",
|
|
321
|
+
source: code,
|
|
322
|
+
});
|
|
323
|
+
if (activeUserId) setUserActiveVisualId(topicId, activeUserId, visualId);
|
|
324
|
+
return visualId;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function storeTopicMediaVisual(input: {
|
|
328
|
+
topicId: string;
|
|
329
|
+
kind: "image" | "video";
|
|
330
|
+
html: string;
|
|
331
|
+
title?: string;
|
|
332
|
+
fileId: string;
|
|
333
|
+
mimeType: string;
|
|
334
|
+
source?: string;
|
|
335
|
+
activeUserId?: string;
|
|
336
|
+
}): number {
|
|
337
|
+
const visualId = insertVisual({
|
|
338
|
+
topicId: input.topicId,
|
|
339
|
+
html: input.html,
|
|
340
|
+
title: normalizeVisualTitle(input.title) ?? null,
|
|
341
|
+
kind: input.kind,
|
|
342
|
+
source: input.source ?? null,
|
|
343
|
+
fileId: input.fileId,
|
|
344
|
+
mimeType: input.mimeType,
|
|
345
|
+
mediaToken: randomUUID(),
|
|
346
|
+
});
|
|
347
|
+
if (input.activeUserId) setUserActiveVisualId(input.topicId, input.activeUserId, visualId);
|
|
348
|
+
return visualId;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Returns info for the system-prompt visual context section, or null if none. */
|
|
352
|
+
export function getActiveVisualForPrompt(
|
|
353
|
+
topicId: string,
|
|
354
|
+
userId: string,
|
|
355
|
+
): {
|
|
356
|
+
kind: VisualKind;
|
|
357
|
+
content: string;
|
|
358
|
+
fence: "html" | "mermaid" | "text";
|
|
359
|
+
title: string | null;
|
|
360
|
+
index: number;
|
|
361
|
+
total: number;
|
|
362
|
+
} | null {
|
|
363
|
+
const rows = listVisuals(topicId);
|
|
364
|
+
if (rows.length === 0) return null;
|
|
365
|
+
const activeId = getUserActiveVisualId(topicId, userId) ?? rows[0]!.id;
|
|
366
|
+
const idx = rows.findIndex((r) => r.id === activeId);
|
|
367
|
+
const targetId = idx >= 0 ? activeId : rows[0]!.id;
|
|
368
|
+
const row = getVisualById(targetId);
|
|
369
|
+
if (!row) return null;
|
|
370
|
+
const total = rows.length;
|
|
371
|
+
const position = idx >= 0 ? idx + 1 : 1;
|
|
372
|
+
if (row.kind === "mermaid") {
|
|
373
|
+
return {
|
|
374
|
+
kind: row.kind,
|
|
375
|
+
content: row.source ?? row.html,
|
|
376
|
+
fence: "mermaid",
|
|
377
|
+
title: row.title,
|
|
378
|
+
index: position,
|
|
379
|
+
total,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (row.kind === "image" || row.kind === "video") {
|
|
383
|
+
return {
|
|
384
|
+
kind: row.kind,
|
|
385
|
+
content: [
|
|
386
|
+
`${row.kind === "image" ? "Image" : "Video"} visual`,
|
|
387
|
+
row.title ? `title: ${row.title}` : null,
|
|
388
|
+
row.file_id ? `file_id: ${row.file_id}` : null,
|
|
389
|
+
row.mime_type ? `mime_type: ${row.mime_type}` : null,
|
|
390
|
+
"Binary media data is omitted from the prompt.",
|
|
391
|
+
]
|
|
392
|
+
.filter(Boolean)
|
|
393
|
+
.join("\n"),
|
|
394
|
+
fence: "text",
|
|
395
|
+
title: row.title,
|
|
396
|
+
index: position,
|
|
397
|
+
total,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
kind: row.kind,
|
|
402
|
+
content: row.html,
|
|
403
|
+
fence: "html",
|
|
404
|
+
title: row.title,
|
|
405
|
+
index: position,
|
|
406
|
+
total,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Delete visual history and per-user selections for a hard-deleted topic. */
|
|
411
|
+
export function deleteTopicVisuals(topicId: string): { visuals: number; views: number } {
|
|
412
|
+
return db.transaction(() => {
|
|
413
|
+
const views = db
|
|
414
|
+
.query("DELETE FROM api_topic_visual_views WHERE topic_id = ?")
|
|
415
|
+
.run(topicId).changes;
|
|
416
|
+
const visuals = db
|
|
417
|
+
.query("DELETE FROM api_topic_visuals WHERE topic_id = ?")
|
|
418
|
+
.run(topicId).changes;
|
|
419
|
+
return { visuals, views };
|
|
420
|
+
})();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** URL that serves the HTML content for a specific visual id (used by iframe). */
|
|
424
|
+
export function topicVisualUrl(topicId: string, vizId: number): string {
|
|
425
|
+
return `/api/v1/topics/${encodeURIComponent(topicId)}/visual/${vizId}/html`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function topicVisualMediaUrl(topicId: string, vizId: number, token: string): string {
|
|
429
|
+
return `/api/v1/topics/${encodeURIComponent(topicId)}/visual/${vizId}/media?token=${encodeURIComponent(token)}`;
|
|
430
|
+
}
|