@geminixiang/mama 0.2.0-beta.6 → 0.2.0-beta.8
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 +20 -14
- package/dist/adapter.d.ts +5 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +1 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +29 -9
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +1 -2
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +8 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +177 -11
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +1 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -1
- package/dist/adapters/slack/branch-manager.js +9 -8
- package/dist/adapters/slack/branch-manager.js.map +1 -1
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +10 -8
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +33 -2
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/agent.d.ts +1 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +507 -422
- package/dist/agent.js.map +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +41 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/model.d.ts +1 -1
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +25 -7
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +1 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +88 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts.map +1 -1
- package/dist/commands/session-view.js +34 -10
- package/dist/commands/session-view.js.map +1 -1
- package/dist/commands/types.d.ts +1 -3
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/utils.d.ts +3 -0
- package/dist/commands/utils.d.ts.map +1 -1
- package/dist/commands/utils.js +5 -0
- package/dist/commands/utils.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -23
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -44
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -210
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -14
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +3 -2
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +40 -7
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/log.d.ts +1 -5
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -38
- package/dist/log.js.map +1 -1
- package/dist/login/index.d.ts +14 -2
- package/dist/login/index.d.ts.map +1 -1
- package/dist/login/index.js +40 -13
- package/dist/login/index.js.map +1 -1
- package/dist/login/portal.d.ts +2 -1
- package/dist/login/portal.d.ts.map +1 -1
- package/dist/login/portal.js +12 -12
- package/dist/login/portal.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +33 -28
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +12 -2
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +43 -14
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts.map +1 -1
- package/dist/runtime/session-runtime.js +49 -148
- package/dist/runtime/session-runtime.js.map +1 -1
- package/dist/sandbox/cloudflare.d.ts.map +1 -1
- package/dist/sandbox/cloudflare.js +2 -2
- package/dist/sandbox/cloudflare.js.map +1 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +1 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +4 -4
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +19 -15
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/portal.d.ts +6 -1
- package/dist/session-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +829 -71
- package/dist/session-view/portal.js.map +1 -1
- package/dist/session-view/service.d.ts.map +1 -1
- package/dist/session-view/service.js +5 -4
- package/dist/session-view/service.js.map +1 -1
- package/dist/session-view/store.d.ts +2 -1
- package/dist/session-view/store.d.ts.map +1 -1
- package/dist/session-view/store.js +2 -1
- package/dist/session-view/store.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +7 -13
- package/dist/store.js.map +1 -1
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +1 -1
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/vault-routing.d.ts +0 -3
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +0 -24
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +21 -57
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +114 -246
- package/dist/vault.js.map +1 -1
- package/package.json +6 -4
- package/dist/bindings.d.ts +0 -45
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -75
- package/dist/bindings.js.map +0 -1
|
@@ -1,6 +1,58 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
1
3
|
import * as log from "../log.js";
|
|
4
|
+
import { inferConversationKind } from "../session-policy.js";
|
|
2
5
|
import { loadSessionViewModel, resolveRequestedSessionFile, } from "./service.js";
|
|
3
|
-
|
|
6
|
+
const markdown = new MarkdownIt({
|
|
7
|
+
html: false,
|
|
8
|
+
linkify: true,
|
|
9
|
+
breaks: true,
|
|
10
|
+
});
|
|
11
|
+
const defaultLinkOpen = markdown.renderer.rules.link_open;
|
|
12
|
+
markdown.renderer.rules.link_open = (...args) => {
|
|
13
|
+
const [tokens, idx, options, env, self] = args;
|
|
14
|
+
const token = tokens[idx];
|
|
15
|
+
token.attrSet("target", "_blank");
|
|
16
|
+
token.attrSet("rel", "noreferrer noopener");
|
|
17
|
+
return defaultLinkOpen
|
|
18
|
+
? defaultLinkOpen(tokens, idx, options, env, self)
|
|
19
|
+
: self.renderToken(tokens, idx, options);
|
|
20
|
+
};
|
|
21
|
+
class SessionViewStreamHub {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.listeners = new Map();
|
|
24
|
+
}
|
|
25
|
+
subscribe(key, listener) {
|
|
26
|
+
const set = this.listeners.get(key) ?? new Set();
|
|
27
|
+
set.add(listener);
|
|
28
|
+
this.listeners.set(key, set);
|
|
29
|
+
return () => {
|
|
30
|
+
const current = this.listeners.get(key);
|
|
31
|
+
if (!current)
|
|
32
|
+
return;
|
|
33
|
+
current.delete(listener);
|
|
34
|
+
if (current.size === 0)
|
|
35
|
+
this.listeners.delete(key);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
publish(key, event) {
|
|
39
|
+
const set = this.listeners.get(key);
|
|
40
|
+
if (!set)
|
|
41
|
+
return;
|
|
42
|
+
for (const listener of set)
|
|
43
|
+
listener(event);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const sessionViewStreamHub = new SessionViewStreamHub();
|
|
47
|
+
export async function handleSessionViewRequest(req, res, url, sessionViewTokenStore, interactive) {
|
|
48
|
+
if (req.method === "POST" && url.pathname === "/session/message") {
|
|
49
|
+
await handleSessionMessageRequest(req, res, sessionViewTokenStore, interactive);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "GET" && url.pathname === "/session/stream") {
|
|
53
|
+
await handleSessionStreamRequest(req, res, url, sessionViewTokenStore, interactive);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
4
56
|
if (req.method !== "GET" || url.pathname !== "/session") {
|
|
5
57
|
return false;
|
|
6
58
|
}
|
|
@@ -34,11 +86,13 @@ export async function handleSessionViewRequest(req, res, url, sessionViewTokenSt
|
|
|
34
86
|
}
|
|
35
87
|
try {
|
|
36
88
|
const model = loadSessionViewModel(targetSessionFile);
|
|
89
|
+
const displayedSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);
|
|
90
|
+
const isRunning = interactive?.handler.isRunning(displayedSessionKey) ?? false;
|
|
37
91
|
res.writeHead(200, {
|
|
38
92
|
"Content-Type": "text/html; charset=utf-8",
|
|
39
93
|
"Cache-Control": "no-store",
|
|
40
94
|
});
|
|
41
|
-
res.end(renderSessionPage(model, entry.token, entry.expiresAt));
|
|
95
|
+
res.end(renderSessionPage(model, entry.token, entry.expiresAt, isRunning, displayedSessionKey, entry.conversationId));
|
|
42
96
|
}
|
|
43
97
|
catch (error) {
|
|
44
98
|
log.logWarning(`[${entry.conversationId}] Failed to render session ${entry.sessionFile}`, error instanceof Error ? error.message : String(error));
|
|
@@ -47,10 +101,26 @@ export async function handleSessionViewRequest(req, res, url, sessionViewTokenSt
|
|
|
47
101
|
}
|
|
48
102
|
return true;
|
|
49
103
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
function resolveDisplayedSessionKey(entry, sessionFile) {
|
|
105
|
+
if (entry.platform === "slack") {
|
|
106
|
+
const fileName = basename(sessionFile, ".jsonl");
|
|
107
|
+
if (/^\d+\.\d+$/.test(fileName)) {
|
|
108
|
+
return `${entry.conversationId}:${fileName}`;
|
|
109
|
+
}
|
|
110
|
+
return entry.conversationId;
|
|
111
|
+
}
|
|
112
|
+
return entry.sessionKey;
|
|
113
|
+
}
|
|
114
|
+
function sessionStreamKey(entry) {
|
|
115
|
+
return `${entry.platform}:${entry.conversationId}:${entry.sessionKey}`;
|
|
116
|
+
}
|
|
117
|
+
function renderTimelineItems(items, token) {
|
|
118
|
+
return items.length > 0
|
|
119
|
+
? items.map((item) => renderItem(item, token)).join("\n")
|
|
53
120
|
: `<div class="system-event"><span class="event-dot"></span><span class="event-text">No messages yet — send one to the bot, then refresh.</span></div>`;
|
|
121
|
+
}
|
|
122
|
+
function renderSessionPage(model, token, expiresAt, isRunning, displayedSessionKey, conversationId) {
|
|
123
|
+
const items = renderTimelineItems(model.items, token);
|
|
54
124
|
const relatedSections = model.parent
|
|
55
125
|
? `<section class="related-card stack">
|
|
56
126
|
<p class="eyebrow">Forked from</p>
|
|
@@ -62,32 +132,50 @@ function renderSessionPage(model, token, expiresAt) {
|
|
|
62
132
|
<div class="hero-title-group">
|
|
63
133
|
<span class="hero-wordmark">mama</span>
|
|
64
134
|
<h1 class="hero-title">${esc(model.title)}</h1>
|
|
135
|
+
<div class="hero-meta-line">
|
|
136
|
+
<span>Created ${esc(formatDate(model.createdAt))}</span>
|
|
137
|
+
<span>Updated <strong data-session-updated>${esc(formatDate(model.updatedAt))}</strong></span>
|
|
138
|
+
<span><strong data-session-entries>${esc(String(model.entryCount))}</strong> entries</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="hero-side">
|
|
142
|
+
<span class="hero-badge hero-badge-status${isRunning ? " is-running" : ""}"><span class="hero-badge-dot"></span><strong data-session-status>${esc(isRunning ? "Running" : "Idle")}</strong></span>
|
|
143
|
+
<span class="hero-badge">${esc(displayedSessionKey === conversationId ? "Channel" : "Thread")}</span>
|
|
65
144
|
</div>
|
|
66
|
-
<button class="refresh-btn" onclick="window.location.reload()">
|
|
67
|
-
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M12.5 2.5A6 6 0 1 0 13 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M10 2.5h2.5V5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
68
|
-
Refresh
|
|
69
|
-
</button>
|
|
70
145
|
</div>
|
|
71
|
-
<div class="
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
${renderSummaryItem("Updated", formatDate(model.updatedAt))}
|
|
76
|
-
${renderSummaryItem("Entries", String(model.entryCount))}
|
|
77
|
-
${renderSummaryItem("Expires", formatDate(new Date(expiresAt).toISOString()))}
|
|
146
|
+
<div class="hero-detail-row">
|
|
147
|
+
<span class="hero-detail"><span class="hero-detail-label">Session</span><code>${esc(model.sessionId.slice(0, 8))}</code></span>
|
|
148
|
+
<span class="hero-detail"><span class="hero-detail-label">File</span><code>${esc(model.fileName)}</code></span>
|
|
149
|
+
<span class="hero-detail"><span class="hero-detail-label">Expires</span><span>${esc(formatDate(new Date(expiresAt).toISOString()))}</span></span>
|
|
78
150
|
</div>
|
|
79
151
|
</header>
|
|
80
152
|
|
|
81
153
|
${relatedSections}
|
|
82
154
|
|
|
83
155
|
<main class="timeline-shell">
|
|
84
|
-
<div class="timeline-list">
|
|
156
|
+
<div class="timeline-list" data-timeline-list>
|
|
85
157
|
${items}
|
|
86
158
|
</div>
|
|
87
|
-
</main
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
159
|
+
</main>
|
|
160
|
+
|
|
161
|
+
<button class="jump-latest-btn" type="button" hidden data-jump-latest aria-label="Jump to latest" title="Jump to latest">↓</button>
|
|
162
|
+
|
|
163
|
+
<section class="composer-card">
|
|
164
|
+
<div class="composer-copy">
|
|
165
|
+
<p class="eyebrow">Interactive preview</p>
|
|
166
|
+
<p>Ask mama in this same session. Replies stay in Session View and do not post back to Slack.</p>
|
|
167
|
+
</div>
|
|
168
|
+
<form class="composer-form" data-session-composer>
|
|
169
|
+
<input type="hidden" name="token" value="${esc(token)}">
|
|
170
|
+
<input type="hidden" name="session" value="${esc(model.fileName)}">
|
|
171
|
+
<input type="hidden" name="sessionKey" value="${esc(displayedSessionKey)}">
|
|
172
|
+
<textarea name="text" rows="3" placeholder="Ask a follow-up…" required></textarea>
|
|
173
|
+
<div class="composer-actions">
|
|
174
|
+
<span class="composer-status" data-composer-status></span>
|
|
175
|
+
<button class="composer-send-btn" type="submit" aria-label="Send" title="Send">↑</button>
|
|
176
|
+
</div>
|
|
177
|
+
</form>
|
|
178
|
+
</section>`, isRunning);
|
|
91
179
|
}
|
|
92
180
|
function renderRelationCard(relation, token) {
|
|
93
181
|
const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;
|
|
@@ -172,7 +260,7 @@ function renderItem(item, token) {
|
|
|
172
260
|
const { username, threadTs, header, content } = parsed;
|
|
173
261
|
const initial = username ? esc(username.slice(0, 2).toUpperCase()) : "U";
|
|
174
262
|
const rawHeader = header ? `<div class="msg-raw-header">${esc(header)}</div>` : "";
|
|
175
|
-
const body = content ?
|
|
263
|
+
const body = content ? renderMarkdownBlock(content, "user") : "";
|
|
176
264
|
const threadBadge = threadTs
|
|
177
265
|
? `<div class="thread-badge" title="Thread ${esc(threadTs)}">Thread · <code>${esc(threadTs)}</code></div>`
|
|
178
266
|
: "";
|
|
@@ -189,7 +277,7 @@ function renderItem(item, token) {
|
|
|
189
277
|
</div>`;
|
|
190
278
|
}
|
|
191
279
|
// assistant
|
|
192
|
-
const body = item.body ?
|
|
280
|
+
const body = item.body ? renderMarkdownBlock(item.body, "assistant") : "";
|
|
193
281
|
const forks = renderForkLinks(item.forks, token ?? "");
|
|
194
282
|
return `<div class="msg-row msg-assistant">
|
|
195
283
|
<div class="msg-avatar asst-avatar" aria-hidden="true">A</div>
|
|
@@ -200,14 +288,251 @@ function renderItem(item, token) {
|
|
|
200
288
|
</div>
|
|
201
289
|
</div>`;
|
|
202
290
|
}
|
|
291
|
+
function renderMarkdownBlock(text, variant) {
|
|
292
|
+
return `<div class="msg-body markdown-body markdown-${variant}">${markdown.render(text)}</div>`;
|
|
293
|
+
}
|
|
294
|
+
function renderLiveUserMessage(text, userName) {
|
|
295
|
+
const initial = esc(userName.slice(0, 2).toUpperCase());
|
|
296
|
+
return `<div class="msg-row msg-user" data-live-item>
|
|
297
|
+
<div class="user-bubble">
|
|
298
|
+
${renderMarkdownBlock(text, "user")}
|
|
299
|
+
</div>
|
|
300
|
+
<div class="msg-avatar user-avatar" title="${esc(userName)}">${initial}</div>
|
|
301
|
+
</div>`;
|
|
302
|
+
}
|
|
303
|
+
function renderLiveAssistantMessage(text) {
|
|
304
|
+
return `<div class="msg-row msg-assistant" data-live-assistant>
|
|
305
|
+
<div class="msg-avatar asst-avatar" aria-hidden="true">A</div>
|
|
306
|
+
<div class="asst-card">
|
|
307
|
+
${renderMarkdownBlock(text, "assistant")}
|
|
308
|
+
</div>
|
|
309
|
+
</div>`;
|
|
310
|
+
}
|
|
311
|
+
function renderLiveToolResult(result) {
|
|
312
|
+
const toneClass = result.isError ? " tone-err" : " tone-ok";
|
|
313
|
+
return `<div class="tool-block" data-live-item>
|
|
314
|
+
<div class="tool-header">
|
|
315
|
+
<span class="tool-icon"><svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M1.5 2L5 5.5 1.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 9h2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
|
|
316
|
+
<span class="tool-name">${esc(result.toolName)}</span>
|
|
317
|
+
</div>
|
|
318
|
+
<pre class="tool-output${toneClass}">${esc(result.result)}</pre>
|
|
319
|
+
</div>`;
|
|
320
|
+
}
|
|
321
|
+
function renderLiveSystemEvent(text, tone = "default") {
|
|
322
|
+
const cls = tone === "err" ? " system-event-err" : "";
|
|
323
|
+
return `<div class="system-event${cls}" data-live-item><span class="event-dot"></span><span class="event-text">${esc(text)}</span></div>`;
|
|
324
|
+
}
|
|
325
|
+
async function handleSessionStreamRequest(req, res, url, sessionViewTokenStore, interactive) {
|
|
326
|
+
const token = url.searchParams.get("token")?.trim() ?? "";
|
|
327
|
+
if (!token || !sessionViewTokenStore || !interactive) {
|
|
328
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
329
|
+
res.end("Session stream unavailable");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const entry = sessionViewTokenStore.peek(token);
|
|
333
|
+
if (!entry) {
|
|
334
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
335
|
+
res.end("Invalid session token");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const requestedSession = url.searchParams.get("session");
|
|
339
|
+
const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
|
|
340
|
+
if (!targetSessionFile) {
|
|
341
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
342
|
+
res.end("Invalid session file");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);
|
|
346
|
+
const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });
|
|
347
|
+
res.writeHead(200, {
|
|
348
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
349
|
+
"Cache-Control": "no-store",
|
|
350
|
+
Connection: "keep-alive",
|
|
351
|
+
});
|
|
352
|
+
res.write(`data: ${JSON.stringify({ type: "status", running: interactive.handler.isRunning(activeSessionKey) })}\n\n`);
|
|
353
|
+
const unsubscribe = sessionViewStreamHub.subscribe(streamKey, (event) => {
|
|
354
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
355
|
+
});
|
|
356
|
+
const heartbeat = setInterval(() => {
|
|
357
|
+
res.write(": keep-alive\n\n");
|
|
358
|
+
}, 15000);
|
|
359
|
+
req.on("close", () => {
|
|
360
|
+
clearInterval(heartbeat);
|
|
361
|
+
unsubscribe();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async function handleSessionMessageRequest(req, res, sessionViewTokenStore, interactive) {
|
|
365
|
+
if (!sessionViewTokenStore || !interactive) {
|
|
366
|
+
json(res, 503, { ok: false, error: "Session chat is not configured." });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
let body;
|
|
370
|
+
try {
|
|
371
|
+
body = JSON.parse(await readRequestBody(req));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
json(res, 400, { ok: false, error: "Invalid request body." });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const token = body.token?.trim() ?? "";
|
|
378
|
+
const text = body.text?.trim() ?? "";
|
|
379
|
+
const requestedSession = body.session?.trim() || null;
|
|
380
|
+
const requestedSessionKey = body.sessionKey?.trim() || "";
|
|
381
|
+
if (!token || !text) {
|
|
382
|
+
json(res, 400, { ok: false, error: "Missing token or text." });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const entry = sessionViewTokenStore.peek(token);
|
|
386
|
+
if (!entry) {
|
|
387
|
+
json(res, 400, { ok: false, error: "This session link is invalid or has expired." });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
|
|
391
|
+
if (!targetSessionFile) {
|
|
392
|
+
json(res, 400, { ok: false, error: "Invalid session file." });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);
|
|
396
|
+
if (requestedSessionKey && requestedSessionKey !== activeSessionKey) {
|
|
397
|
+
json(res, 400, { ok: false, error: "Session target mismatch." });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const bot = interactive.botsByPlatform[entry.platform];
|
|
401
|
+
if (!bot) {
|
|
402
|
+
json(res, 503, { ok: false, error: `No bot configured for ${entry.platform}.` });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });
|
|
406
|
+
const conversationKind = inferConversationKind(entry.platform, entry.conversationId);
|
|
407
|
+
const ts = (Date.now() / 1000).toFixed(6);
|
|
408
|
+
const platformInfo = bot.getPlatformInfo();
|
|
409
|
+
const platformUserName = entry.platformUserName ||
|
|
410
|
+
platformInfo.users.find((user) => user.id === entry.platformUserId)?.userName ||
|
|
411
|
+
platformInfo.users.find((user) => user.id === entry.platformUserId)?.displayName ||
|
|
412
|
+
"unknown";
|
|
413
|
+
const responseCtx = createSessionViewResponseContext((event) => {
|
|
414
|
+
sessionViewStreamHub.publish(streamKey, event);
|
|
415
|
+
});
|
|
416
|
+
const event = {
|
|
417
|
+
type: "session_view",
|
|
418
|
+
conversationId: entry.conversationId,
|
|
419
|
+
conversationKind,
|
|
420
|
+
ts,
|
|
421
|
+
user: entry.platformUserId,
|
|
422
|
+
text,
|
|
423
|
+
attachments: [],
|
|
424
|
+
sessionKey: activeSessionKey,
|
|
425
|
+
...(activeSessionKey.includes(":")
|
|
426
|
+
? { thread_ts: activeSessionKey.split(":").slice(1).join(":") }
|
|
427
|
+
: {}),
|
|
428
|
+
};
|
|
429
|
+
const adapters = {
|
|
430
|
+
message: {
|
|
431
|
+
id: ts,
|
|
432
|
+
sessionKey: activeSessionKey,
|
|
433
|
+
conversationKind,
|
|
434
|
+
userId: entry.platformUserId,
|
|
435
|
+
userName: platformUserName,
|
|
436
|
+
text,
|
|
437
|
+
attachments: [],
|
|
438
|
+
threadTs: event.thread_ts,
|
|
439
|
+
},
|
|
440
|
+
responseCtx,
|
|
441
|
+
platform: { ...platformInfo, diagnostics: { showUsageSummary: false } },
|
|
442
|
+
};
|
|
443
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: true });
|
|
444
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
445
|
+
type: "user",
|
|
446
|
+
html: renderLiveUserMessage(text, platformUserName),
|
|
447
|
+
});
|
|
448
|
+
void interactive.handler
|
|
449
|
+
.handleEvent(event, bot, adapters, false)
|
|
450
|
+
.then(() => {
|
|
451
|
+
if (!targetSessionFile) {
|
|
452
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: false });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const model = loadSessionViewModel(targetSessionFile);
|
|
456
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
457
|
+
type: "refresh",
|
|
458
|
+
timelineHtml: renderTimelineItems(model.items, token),
|
|
459
|
+
updatedAt: formatDate(model.updatedAt),
|
|
460
|
+
entryCount: model.entryCount,
|
|
461
|
+
running: false,
|
|
462
|
+
});
|
|
463
|
+
})
|
|
464
|
+
.catch((error) => {
|
|
465
|
+
log.logWarning(`[${entry.conversationId}] Session view message failed`, error instanceof Error ? error.message : String(error));
|
|
466
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
467
|
+
type: "error",
|
|
468
|
+
message: error instanceof Error ? error.message : String(error),
|
|
469
|
+
});
|
|
470
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: false });
|
|
471
|
+
});
|
|
472
|
+
json(res, 202, { ok: true, accepted: true });
|
|
473
|
+
}
|
|
474
|
+
function createSessionViewResponseContext(publish) {
|
|
475
|
+
let accumulatedText = "";
|
|
476
|
+
return {
|
|
477
|
+
respond: async (text) => {
|
|
478
|
+
accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
|
|
479
|
+
publish({ type: "assistant", html: renderLiveAssistantMessage(accumulatedText) });
|
|
480
|
+
},
|
|
481
|
+
replaceResponse: async (text) => {
|
|
482
|
+
accumulatedText = text;
|
|
483
|
+
publish({ type: "assistant", html: renderLiveAssistantMessage(accumulatedText) });
|
|
484
|
+
},
|
|
485
|
+
respondDiagnostic: async (text, options) => {
|
|
486
|
+
if (options?.style === "error") {
|
|
487
|
+
publish({ type: "system", html: renderLiveSystemEvent(text, "err") });
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
respondToolResult: async (result) => {
|
|
491
|
+
publish({ type: "tool", html: renderLiveToolResult(result) });
|
|
492
|
+
},
|
|
493
|
+
setTyping: async () => {
|
|
494
|
+
publish({ type: "status", running: true });
|
|
495
|
+
},
|
|
496
|
+
setWorking: async (working) => {
|
|
497
|
+
publish({ type: "status", running: working });
|
|
498
|
+
},
|
|
499
|
+
uploadFile: async () => { },
|
|
500
|
+
deleteResponse: async () => {
|
|
501
|
+
accumulatedText = "";
|
|
502
|
+
publish({ type: "assistant_remove" });
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function readRequestBody(req) {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
let data = "";
|
|
509
|
+
req.setEncoding("utf8");
|
|
510
|
+
req.on("data", (chunk) => {
|
|
511
|
+
data += chunk;
|
|
512
|
+
if (data.length > 1024 * 1024) {
|
|
513
|
+
reject(new Error("Request body too large"));
|
|
514
|
+
req.destroy();
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
req.on("end", () => resolve(data));
|
|
518
|
+
req.on("error", reject);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function json(res, status, body) {
|
|
522
|
+
res.writeHead(status, {
|
|
523
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
524
|
+
"Cache-Control": "no-store",
|
|
525
|
+
});
|
|
526
|
+
res.end(JSON.stringify(body));
|
|
527
|
+
}
|
|
203
528
|
function renderStatusPage(title, message) {
|
|
204
529
|
return renderHtmlDocument(title, `<section class="card stack">
|
|
205
530
|
<p class="eyebrow">mama</p>
|
|
206
531
|
<h1>${esc(title)}</h1>
|
|
207
532
|
<div class="status err">${esc(message)}</div>
|
|
208
|
-
</section
|
|
533
|
+
</section>`, false);
|
|
209
534
|
}
|
|
210
|
-
function renderHtmlDocument(title, shellContent) {
|
|
535
|
+
function renderHtmlDocument(title, shellContent, isRunning) {
|
|
211
536
|
return `<!DOCTYPE html>
|
|
212
537
|
<html lang="en">
|
|
213
538
|
<head>
|
|
@@ -216,10 +541,154 @@ function renderHtmlDocument(title, shellContent) {
|
|
|
216
541
|
<title>${esc(title)}</title>
|
|
217
542
|
<style>${styles}</style>
|
|
218
543
|
</head>
|
|
219
|
-
<body>
|
|
544
|
+
<body data-session-running="${isRunning ? "true" : "false"}">
|
|
220
545
|
<main class="shell">
|
|
221
546
|
${shellContent}
|
|
222
547
|
</main>
|
|
548
|
+
<script>
|
|
549
|
+
const form = document.querySelector('[data-session-composer]');
|
|
550
|
+
const timelineList = document.querySelector('[data-timeline-list]');
|
|
551
|
+
const jumpLatestBtn = document.querySelector('[data-jump-latest]');
|
|
552
|
+
const statusEl = document.querySelector('[data-session-status]');
|
|
553
|
+
const updatedEl = document.querySelector('[data-session-updated]');
|
|
554
|
+
const entriesEl = document.querySelector('[data-session-entries]');
|
|
555
|
+
const composerStatus = form?.querySelector('[data-composer-status]');
|
|
556
|
+
const textarea = form?.querySelector('textarea[name="text"]');
|
|
557
|
+
const submitButton = form?.querySelector('button[type="submit"]');
|
|
558
|
+
let liveAssistant = null;
|
|
559
|
+
let running = document.body.dataset.sessionRunning === 'true';
|
|
560
|
+
|
|
561
|
+
const isNearBottom = () => window.innerHeight + window.scrollY >= document.body.offsetHeight - 120;
|
|
562
|
+
const scrollToLatest = (behavior = 'smooth') => window.scrollTo({ top: document.body.scrollHeight, behavior });
|
|
563
|
+
const toggleJumpButton = () => {
|
|
564
|
+
if (!jumpLatestBtn) return;
|
|
565
|
+
jumpLatestBtn.hidden = isNearBottom();
|
|
566
|
+
};
|
|
567
|
+
const updateFollowState = () => {
|
|
568
|
+
if (isNearBottom()) scrollToLatest('smooth');
|
|
569
|
+
else toggleJumpButton();
|
|
570
|
+
};
|
|
571
|
+
const canSubmit = () => Boolean(textarea && textarea.value.trim()) && !running;
|
|
572
|
+
const updateSubmitButtonState = () => {
|
|
573
|
+
if (submitButton) submitButton.disabled = !canSubmit();
|
|
574
|
+
};
|
|
575
|
+
const setRunning = (value) => {
|
|
576
|
+
running = value;
|
|
577
|
+
document.body.dataset.sessionRunning = value ? 'true' : 'false';
|
|
578
|
+
if (statusEl) statusEl.textContent = value ? 'Running' : 'Idle';
|
|
579
|
+
updateSubmitButtonState();
|
|
580
|
+
if (composerStatus && !value && composerStatus.textContent === 'Thinking…') {
|
|
581
|
+
composerStatus.textContent = '';
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
jumpLatestBtn?.addEventListener('click', () => {
|
|
586
|
+
scrollToLatest('smooth');
|
|
587
|
+
toggleJumpButton();
|
|
588
|
+
});
|
|
589
|
+
window.addEventListener('scroll', toggleJumpButton, { passive: true });
|
|
590
|
+
|
|
591
|
+
if (textarea) {
|
|
592
|
+
const resize = () => {
|
|
593
|
+
textarea.style.height = 'auto';
|
|
594
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 240) + 'px';
|
|
595
|
+
};
|
|
596
|
+
textarea.addEventListener('input', () => {
|
|
597
|
+
resize();
|
|
598
|
+
updateSubmitButtonState();
|
|
599
|
+
});
|
|
600
|
+
textarea.addEventListener('keydown', (event) => {
|
|
601
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
if (!running) form?.requestSubmit();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
resize();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
setRunning(running);
|
|
610
|
+
updateSubmitButtonState();
|
|
611
|
+
|
|
612
|
+
const streamUrl = form
|
|
613
|
+
? '/session/stream?token=' + encodeURIComponent(form.token.value) + '&session=' + encodeURIComponent(form.session.value)
|
|
614
|
+
: null;
|
|
615
|
+
if (streamUrl) {
|
|
616
|
+
const source = new EventSource(streamUrl);
|
|
617
|
+
source.onmessage = (event) => {
|
|
618
|
+
const payload = JSON.parse(event.data);
|
|
619
|
+
switch (payload.type) {
|
|
620
|
+
case 'status':
|
|
621
|
+
setRunning(Boolean(payload.running));
|
|
622
|
+
if (payload.running && composerStatus) composerStatus.textContent = 'Thinking…';
|
|
623
|
+
break;
|
|
624
|
+
case 'user':
|
|
625
|
+
case 'tool':
|
|
626
|
+
case 'system': {
|
|
627
|
+
timelineList?.insertAdjacentHTML('beforeend', payload.html);
|
|
628
|
+
updateFollowState();
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'assistant': {
|
|
632
|
+
if (!liveAssistant || !liveAssistant.isConnected) {
|
|
633
|
+
timelineList?.insertAdjacentHTML('beforeend', payload.html);
|
|
634
|
+
liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;
|
|
635
|
+
} else {
|
|
636
|
+
liveAssistant.outerHTML = payload.html;
|
|
637
|
+
liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;
|
|
638
|
+
}
|
|
639
|
+
updateFollowState();
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'assistant_remove':
|
|
643
|
+
if (liveAssistant?.isConnected) liveAssistant.remove();
|
|
644
|
+
liveAssistant = null;
|
|
645
|
+
break;
|
|
646
|
+
case 'refresh':
|
|
647
|
+
if (timelineList) timelineList.innerHTML = payload.timelineHtml;
|
|
648
|
+
liveAssistant = null;
|
|
649
|
+
if (updatedEl) updatedEl.textContent = payload.updatedAt;
|
|
650
|
+
if (entriesEl) entriesEl.textContent = String(payload.entryCount);
|
|
651
|
+
setRunning(Boolean(payload.running));
|
|
652
|
+
if (composerStatus) composerStatus.textContent = '';
|
|
653
|
+
updateFollowState();
|
|
654
|
+
break;
|
|
655
|
+
case 'error':
|
|
656
|
+
if (composerStatus) composerStatus.textContent = payload.message || 'Something went wrong';
|
|
657
|
+
setRunning(false);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
form?.addEventListener('submit', async (event) => {
|
|
664
|
+
event.preventDefault();
|
|
665
|
+
if (!textarea || !composerStatus) return;
|
|
666
|
+
const text = textarea.value.trim();
|
|
667
|
+
if (!text || running) return;
|
|
668
|
+
composerStatus.textContent = 'Sending…';
|
|
669
|
+
updateSubmitButtonState();
|
|
670
|
+
try {
|
|
671
|
+
const response = await fetch('/session/message', {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify({ token: form.token.value, session: form.session.value, sessionKey: form.sessionKey.value, text }),
|
|
675
|
+
});
|
|
676
|
+
const payload = await response.json();
|
|
677
|
+
if (!response.ok || !payload.ok) throw new Error(payload.error || 'Request failed');
|
|
678
|
+
textarea.value = '';
|
|
679
|
+
textarea.style.height = 'auto';
|
|
680
|
+
composerStatus.textContent = 'Thinking…';
|
|
681
|
+
setRunning(true);
|
|
682
|
+
updateSubmitButtonState();
|
|
683
|
+
scrollToLatest('smooth');
|
|
684
|
+
} catch (err) {
|
|
685
|
+
composerStatus.textContent = err && err.message ? err.message : String(err);
|
|
686
|
+
submitButton.disabled = false;
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
toggleJumpButton();
|
|
691
|
+
</script>
|
|
223
692
|
</body>
|
|
224
693
|
</html>`;
|
|
225
694
|
}
|
|
@@ -278,6 +747,7 @@ const styles = `
|
|
|
278
747
|
display: flex;
|
|
279
748
|
flex-direction: column;
|
|
280
749
|
align-items: center;
|
|
750
|
+
overflow-x: hidden;
|
|
281
751
|
background-color: var(--bg);
|
|
282
752
|
background-image:
|
|
283
753
|
radial-gradient(ellipse 80% 40% at 50% -10%, rgba(255,255,255,0.6) 0%, transparent 70%);
|
|
@@ -291,6 +761,7 @@ const styles = `
|
|
|
291
761
|
.shell {
|
|
292
762
|
width: 100%;
|
|
293
763
|
max-width: 780px;
|
|
764
|
+
min-width: 0;
|
|
294
765
|
display: flex;
|
|
295
766
|
flex-direction: column;
|
|
296
767
|
gap: 12px;
|
|
@@ -310,8 +781,8 @@ const styles = `
|
|
|
310
781
|
display: flex;
|
|
311
782
|
align-items: flex-start;
|
|
312
783
|
justify-content: space-between;
|
|
313
|
-
gap:
|
|
314
|
-
margin-bottom:
|
|
784
|
+
gap: 20px;
|
|
785
|
+
margin-bottom: 18px;
|
|
315
786
|
}
|
|
316
787
|
|
|
317
788
|
.hero-wordmark {
|
|
@@ -332,61 +803,103 @@ const styles = `
|
|
|
332
803
|
letter-spacing: -0.01em;
|
|
333
804
|
color: var(--text);
|
|
334
805
|
text-wrap: balance;
|
|
806
|
+
margin-bottom: 8px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.hero-meta-line {
|
|
810
|
+
display: flex;
|
|
811
|
+
flex-wrap: wrap;
|
|
812
|
+
gap: 8px 14px;
|
|
813
|
+
color: var(--muted);
|
|
814
|
+
font-size: 0.82rem;
|
|
815
|
+
line-height: 1.4;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.hero-meta-line strong {
|
|
819
|
+
color: var(--text);
|
|
820
|
+
font-weight: 600;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.hero-side {
|
|
824
|
+
display: flex;
|
|
825
|
+
flex-direction: column;
|
|
826
|
+
align-items: flex-end;
|
|
827
|
+
gap: 8px;
|
|
828
|
+
flex-shrink: 0;
|
|
335
829
|
}
|
|
336
830
|
|
|
337
|
-
.
|
|
831
|
+
.hero-badge {
|
|
338
832
|
display: inline-flex;
|
|
339
833
|
align-items: center;
|
|
340
|
-
gap:
|
|
341
|
-
|
|
342
|
-
padding: 7px 14px;
|
|
834
|
+
gap: 8px;
|
|
835
|
+
padding: 6px 11px;
|
|
343
836
|
border: 1px solid var(--border);
|
|
344
837
|
border-radius: 999px;
|
|
345
|
-
background:
|
|
838
|
+
background: rgba(255,255,255,0.7);
|
|
839
|
+
font-size: 0.78rem;
|
|
346
840
|
color: var(--muted);
|
|
347
|
-
|
|
348
|
-
cursor: pointer;
|
|
349
|
-
transition: color 120ms, border-color 120ms, background 120ms;
|
|
350
|
-
white-space: nowrap;
|
|
841
|
+
line-height: 1;
|
|
351
842
|
}
|
|
352
843
|
|
|
353
|
-
.
|
|
844
|
+
.hero-badge strong {
|
|
354
845
|
color: var(--text);
|
|
355
|
-
|
|
356
|
-
|
|
846
|
+
font-weight: 600;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.hero-badge-status.is-running {
|
|
850
|
+
background: #fff7ed;
|
|
851
|
+
border-color: rgba(217, 119, 6, 0.18);
|
|
852
|
+
color: #9a3412;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.hero-badge-dot {
|
|
856
|
+
width: 7px;
|
|
857
|
+
height: 7px;
|
|
858
|
+
border-radius: 50%;
|
|
859
|
+
background: #a1a1aa;
|
|
860
|
+
flex-shrink: 0;
|
|
357
861
|
}
|
|
358
862
|
|
|
359
|
-
.
|
|
360
|
-
|
|
361
|
-
|
|
863
|
+
.hero-badge-status.is-running .hero-badge-dot {
|
|
864
|
+
background: #d97706;
|
|
865
|
+
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.14);
|
|
362
866
|
}
|
|
363
867
|
|
|
364
|
-
.
|
|
868
|
+
.hero-detail-row {
|
|
365
869
|
display: flex;
|
|
366
870
|
flex-wrap: wrap;
|
|
367
|
-
gap:
|
|
871
|
+
gap: 10px;
|
|
872
|
+
padding-top: 14px;
|
|
873
|
+
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
368
874
|
}
|
|
369
875
|
|
|
370
|
-
.
|
|
876
|
+
.hero-detail {
|
|
371
877
|
display: inline-flex;
|
|
372
878
|
align-items: center;
|
|
373
|
-
gap:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
border-radius:
|
|
377
|
-
background:
|
|
378
|
-
|
|
379
|
-
|
|
879
|
+
gap: 8px;
|
|
880
|
+
min-width: 0;
|
|
881
|
+
padding: 6px 10px;
|
|
882
|
+
border-radius: 12px;
|
|
883
|
+
background: rgba(0, 0, 0, 0.025);
|
|
884
|
+
color: var(--muted);
|
|
885
|
+
font-size: 0.78rem;
|
|
380
886
|
}
|
|
381
887
|
|
|
382
|
-
.
|
|
383
|
-
|
|
384
|
-
|
|
888
|
+
.hero-detail-label {
|
|
889
|
+
text-transform: uppercase;
|
|
890
|
+
letter-spacing: 0.08em;
|
|
891
|
+
font-size: 0.68rem;
|
|
892
|
+
color: var(--subtle);
|
|
385
893
|
}
|
|
386
894
|
|
|
387
|
-
.
|
|
895
|
+
.hero-detail code {
|
|
896
|
+
min-width: 0;
|
|
897
|
+
overflow: hidden;
|
|
898
|
+
text-overflow: ellipsis;
|
|
899
|
+
white-space: nowrap;
|
|
900
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
901
|
+
font-size: 0.74rem;
|
|
388
902
|
color: var(--text);
|
|
389
|
-
font-weight: 600;
|
|
390
903
|
}
|
|
391
904
|
|
|
392
905
|
/* ── Timeline shell ───────────────────────────────────────────────────── */
|
|
@@ -508,6 +1021,7 @@ const styles = `
|
|
|
508
1021
|
display: flex;
|
|
509
1022
|
flex-direction: column;
|
|
510
1023
|
gap: 4px;
|
|
1024
|
+
min-width: 0;
|
|
511
1025
|
}
|
|
512
1026
|
|
|
513
1027
|
/* ── Message rows ─────────────────────────────────────────────────────── */
|
|
@@ -517,6 +1031,7 @@ const styles = `
|
|
|
517
1031
|
align-items: flex-end;
|
|
518
1032
|
gap: 8px;
|
|
519
1033
|
padding: 2px 0;
|
|
1034
|
+
min-width: 0;
|
|
520
1035
|
}
|
|
521
1036
|
|
|
522
1037
|
/* ── User messages ────────────────────────────────────────────────────── */
|
|
@@ -527,6 +1042,7 @@ const styles = `
|
|
|
527
1042
|
|
|
528
1043
|
.user-bubble {
|
|
529
1044
|
max-width: 85%;
|
|
1045
|
+
min-width: 0;
|
|
530
1046
|
padding: 12px 16px;
|
|
531
1047
|
border-radius: 18px 18px 4px 18px;
|
|
532
1048
|
background: var(--user-bg);
|
|
@@ -568,11 +1084,6 @@ const styles = `
|
|
|
568
1084
|
}
|
|
569
1085
|
|
|
570
1086
|
.msg-user .msg-body {
|
|
571
|
-
font-family: 'DM Sans', system-ui, sans-serif;
|
|
572
|
-
font-size: 0.9rem;
|
|
573
|
-
line-height: 1.6;
|
|
574
|
-
white-space: pre-wrap;
|
|
575
|
-
word-break: break-word;
|
|
576
1087
|
color: var(--user-text);
|
|
577
1088
|
}
|
|
578
1089
|
|
|
@@ -623,6 +1134,7 @@ const styles = `
|
|
|
623
1134
|
|
|
624
1135
|
.asst-card {
|
|
625
1136
|
min-width: 0;
|
|
1137
|
+
max-width: 100%;
|
|
626
1138
|
padding: 14px 18px;
|
|
627
1139
|
border: 1px solid var(--border);
|
|
628
1140
|
border-radius: 18px 18px 18px 4px;
|
|
@@ -631,11 +1143,6 @@ const styles = `
|
|
|
631
1143
|
}
|
|
632
1144
|
|
|
633
1145
|
.msg-assistant .msg-body {
|
|
634
|
-
font-family: 'DM Sans', system-ui, sans-serif;
|
|
635
|
-
font-size: 0.9rem;
|
|
636
|
-
line-height: 1.65;
|
|
637
|
-
white-space: pre-wrap;
|
|
638
|
-
word-break: break-word;
|
|
639
1146
|
color: var(--text);
|
|
640
1147
|
}
|
|
641
1148
|
|
|
@@ -711,6 +1218,138 @@ const styles = `
|
|
|
711
1218
|
.tool-output.tone-ok { color: var(--tool-ok); }
|
|
712
1219
|
.tool-output.tone-err { color: var(--tool-err); }
|
|
713
1220
|
|
|
1221
|
+
/* ── Markdown blocks ──────────────────────────────────────────────────── */
|
|
1222
|
+
|
|
1223
|
+
.markdown-body {
|
|
1224
|
+
font-family: 'DM Sans', system-ui, sans-serif;
|
|
1225
|
+
font-size: 0.9rem;
|
|
1226
|
+
line-height: 1.65;
|
|
1227
|
+
word-break: break-word;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.markdown-body > *:first-child { margin-top: 0; }
|
|
1231
|
+
.markdown-body > *:last-child { margin-bottom: 0; }
|
|
1232
|
+
.markdown-body p,
|
|
1233
|
+
.markdown-body ul,
|
|
1234
|
+
.markdown-body ol,
|
|
1235
|
+
.markdown-body blockquote,
|
|
1236
|
+
.markdown-body pre,
|
|
1237
|
+
.markdown-body table,
|
|
1238
|
+
.markdown-body hr {
|
|
1239
|
+
margin: 0 0 0.85em;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.markdown-body h1,
|
|
1243
|
+
.markdown-body h2,
|
|
1244
|
+
.markdown-body h3,
|
|
1245
|
+
.markdown-body h4,
|
|
1246
|
+
.markdown-body h5,
|
|
1247
|
+
.markdown-body h6 {
|
|
1248
|
+
margin: 0 0 0.55em;
|
|
1249
|
+
line-height: 1.25;
|
|
1250
|
+
font-weight: 700;
|
|
1251
|
+
letter-spacing: -0.01em;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.markdown-body h1 { font-size: 1.4rem; }
|
|
1255
|
+
.markdown-body h2 { font-size: 1.22rem; }
|
|
1256
|
+
.markdown-body h3 { font-size: 1.08rem; }
|
|
1257
|
+
.markdown-body h4,
|
|
1258
|
+
.markdown-body h5,
|
|
1259
|
+
.markdown-body h6 { font-size: 0.95rem; }
|
|
1260
|
+
|
|
1261
|
+
.markdown-body ul,
|
|
1262
|
+
.markdown-body ol {
|
|
1263
|
+
padding-left: 1.3em;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.markdown-body li + li {
|
|
1267
|
+
margin-top: 0.22em;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.markdown-body blockquote {
|
|
1271
|
+
padding-left: 12px;
|
|
1272
|
+
border-left: 3px solid rgba(34, 197, 94, 0.35);
|
|
1273
|
+
opacity: 0.95;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.markdown-body a {
|
|
1277
|
+
color: inherit;
|
|
1278
|
+
text-decoration: underline;
|
|
1279
|
+
text-underline-offset: 2px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.markdown-body code {
|
|
1283
|
+
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
|
1284
|
+
font-size: 0.82em;
|
|
1285
|
+
padding: 0.16em 0.38em;
|
|
1286
|
+
border-radius: 6px;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.markdown-body pre {
|
|
1290
|
+
overflow-x: auto;
|
|
1291
|
+
border-radius: 12px;
|
|
1292
|
+
padding: 12px 14px;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.markdown-body pre code {
|
|
1296
|
+
display: block;
|
|
1297
|
+
padding: 0;
|
|
1298
|
+
border-radius: 0;
|
|
1299
|
+
background: transparent;
|
|
1300
|
+
font-size: 0.82rem;
|
|
1301
|
+
line-height: 1.6;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.markdown-body table {
|
|
1305
|
+
width: 100%;
|
|
1306
|
+
border-collapse: collapse;
|
|
1307
|
+
font-size: 0.85rem;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.markdown-body th,
|
|
1311
|
+
.markdown-body td {
|
|
1312
|
+
padding: 8px 10px;
|
|
1313
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
1314
|
+
text-align: left;
|
|
1315
|
+
vertical-align: top;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.markdown-body img {
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
border-radius: 12px;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.markdown-user code {
|
|
1324
|
+
background: rgba(255,255,255,0.14);
|
|
1325
|
+
color: var(--user-text);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.markdown-user pre {
|
|
1329
|
+
background: rgba(255,255,255,0.08);
|
|
1330
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.markdown-user table th,
|
|
1334
|
+
.markdown-user table td {
|
|
1335
|
+
border-color: rgba(255,255,255,0.16);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.markdown-assistant code {
|
|
1339
|
+
background: #f4f4f5;
|
|
1340
|
+
color: #27272a;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.markdown-assistant pre {
|
|
1344
|
+
background: #0f172a;
|
|
1345
|
+
color: #e5e7eb;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.markdown-assistant table th,
|
|
1349
|
+
.markdown-assistant table td {
|
|
1350
|
+
border-color: rgba(0, 0, 0, 0.08);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
714
1353
|
/* ── System events ────────────────────────────────────────────────────── */
|
|
715
1354
|
|
|
716
1355
|
.system-event {
|
|
@@ -736,6 +1375,10 @@ const styles = `
|
|
|
736
1375
|
color: var(--muted);
|
|
737
1376
|
}
|
|
738
1377
|
|
|
1378
|
+
.system-event-err .event-text {
|
|
1379
|
+
color: var(--err-text);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
739
1382
|
.event-time {
|
|
740
1383
|
color: var(--subtle);
|
|
741
1384
|
font-style: normal;
|
|
@@ -783,6 +1426,119 @@ const styles = `
|
|
|
783
1426
|
border: 1px solid rgba(185, 28, 28, 0.12);
|
|
784
1427
|
}
|
|
785
1428
|
|
|
1429
|
+
/* ── Composer ─────────────────────────────────────────────────────────── */
|
|
1430
|
+
|
|
1431
|
+
.composer-card {
|
|
1432
|
+
padding: 8px 2px 0;
|
|
1433
|
+
border: none;
|
|
1434
|
+
border-radius: 0;
|
|
1435
|
+
background: transparent;
|
|
1436
|
+
box-shadow: none;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.jump-latest-btn {
|
|
1440
|
+
position: fixed;
|
|
1441
|
+
left: 50%;
|
|
1442
|
+
bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
|
|
1443
|
+
z-index: 10;
|
|
1444
|
+
width: 42px;
|
|
1445
|
+
height: 42px;
|
|
1446
|
+
border: 1px solid var(--border);
|
|
1447
|
+
border-radius: 999px;
|
|
1448
|
+
background: var(--bg);
|
|
1449
|
+
color: var(--text);
|
|
1450
|
+
font: 700 1rem/1 'DM Sans', sans-serif;
|
|
1451
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
|
|
1452
|
+
cursor: pointer;
|
|
1453
|
+
backdrop-filter: blur(10px);
|
|
1454
|
+
transform: translateX(-50%);
|
|
1455
|
+
outline: none;
|
|
1456
|
+
appearance: none;
|
|
1457
|
+
-webkit-tap-highlight-color: transparent;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.jump-latest-btn:hover {
|
|
1461
|
+
transform: translateX(-50%) translateY(-1px);
|
|
1462
|
+
background: #e8e3d9;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.jump-latest-btn:focus,
|
|
1466
|
+
.jump-latest-btn:active {
|
|
1467
|
+
outline: none;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
.jump-latest-btn:focus-visible {
|
|
1471
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.12), 0 0 0 3px rgba(0,0,0,0.08);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
.composer-copy { margin-bottom: 12px; color: var(--muted); }
|
|
1475
|
+
|
|
1476
|
+
.composer-form textarea {
|
|
1477
|
+
width: 100%;
|
|
1478
|
+
resize: none;
|
|
1479
|
+
overflow-y: auto;
|
|
1480
|
+
min-height: 84px;
|
|
1481
|
+
padding: 12px 14px;
|
|
1482
|
+
border: 1px solid var(--border);
|
|
1483
|
+
border-radius: 14px;
|
|
1484
|
+
font: inherit;
|
|
1485
|
+
color: var(--text);
|
|
1486
|
+
background: #fafafa;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.composer-form textarea:focus {
|
|
1490
|
+
outline: 2px solid rgba(34, 197, 94, 0.18);
|
|
1491
|
+
border-color: rgba(34, 197, 94, 0.45);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
.composer-actions {
|
|
1495
|
+
display: flex;
|
|
1496
|
+
align-items: center;
|
|
1497
|
+
justify-content: space-between;
|
|
1498
|
+
gap: 12px;
|
|
1499
|
+
margin-top: 10px;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.composer-status { color: var(--muted); font-size: 13px; }
|
|
1503
|
+
.composer-actions button:disabled { opacity: 0.55; cursor: wait; }
|
|
1504
|
+
|
|
1505
|
+
.composer-send-btn {
|
|
1506
|
+
display: inline-flex;
|
|
1507
|
+
align-items: center;
|
|
1508
|
+
justify-content: center;
|
|
1509
|
+
width: 42px;
|
|
1510
|
+
height: 42px;
|
|
1511
|
+
border: none;
|
|
1512
|
+
border-radius: 999px;
|
|
1513
|
+
background: #d97706;
|
|
1514
|
+
color: #ffffff;
|
|
1515
|
+
font: 700 1rem/1 'DM Sans', sans-serif;
|
|
1516
|
+
cursor: pointer;
|
|
1517
|
+
box-shadow: 0 10px 24px rgba(217, 119, 6, 0.26);
|
|
1518
|
+
transition: transform 120ms, filter 120ms, box-shadow 120ms, background 120ms;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.composer-send-btn:hover:not(:disabled) {
|
|
1522
|
+
transform: translateY(-1px);
|
|
1523
|
+
filter: saturate(1.06) brightness(0.98);
|
|
1524
|
+
box-shadow: 0 12px 28px rgba(217, 119, 6, 0.32);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.composer-send-btn:focus-visible {
|
|
1528
|
+
outline: 2px solid rgba(217, 119, 6, 0.28);
|
|
1529
|
+
outline-offset: 3px;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.composer-send-btn:disabled {
|
|
1533
|
+
background: #d4d4d8;
|
|
1534
|
+
color: rgba(24, 24, 27, 0.45);
|
|
1535
|
+
box-shadow: none;
|
|
1536
|
+
transform: none;
|
|
1537
|
+
filter: none;
|
|
1538
|
+
cursor: not-allowed;
|
|
1539
|
+
opacity: 1;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
786
1542
|
/* ── Responsive ───────────────────────────────────────────────────────── */
|
|
787
1543
|
|
|
788
1544
|
@media (max-width: 600px) {
|
|
@@ -791,10 +1547,12 @@ const styles = `
|
|
|
791
1547
|
.hero-card, .card { padding: 20px; border-radius: 16px; }
|
|
792
1548
|
|
|
793
1549
|
.hero-top { flex-direction: column; gap: 12px; }
|
|
1550
|
+
.hero-side { align-items: flex-start; }
|
|
1551
|
+
.hero-detail-row { gap: 8px; }
|
|
794
1552
|
|
|
795
|
-
.
|
|
796
|
-
|
|
797
|
-
.
|
|
1553
|
+
.user-bubble,
|
|
1554
|
+
.msg-assistant,
|
|
1555
|
+
.tool-block { max-width: 100%; }
|
|
798
1556
|
|
|
799
1557
|
.asst-avatar { display: none; }
|
|
800
1558
|
|