@geminixiang/mama 0.2.0-beta.7 → 0.2.0-beta.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 +3 -5
- package/dist/adapter.d.ts +2 -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 +7 -4
- 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.map +1 -1
- package/dist/adapters/slack/bot.js +20 -6
- 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 -13
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +2 -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 +477 -408
- package/dist/agent.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/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 +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +25 -2
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/session-view.d.ts.map +1 -1
- package/dist/commands/session-view.js +5 -1
- 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/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -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 -225
- 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 +26 -27
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +0 -2
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +2 -4
- 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 +1 -0
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +18 -14
- 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 +1027 -89
- 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 +4 -3
- 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/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 +3 -1
- 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="1" placeholder="Write a message…" 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)}`;
|
|
@@ -143,6 +231,9 @@ export function parseUserBody(raw) {
|
|
|
143
231
|
}
|
|
144
232
|
return { timestamp: null, username: null, threadTs: null, header: null, content: raw };
|
|
145
233
|
}
|
|
234
|
+
function renderCopyButton(label = "Copy message") {
|
|
235
|
+
return `<div class="msg-actions"><button class="copy-action-btn" type="button" data-copy-button data-copy-label="${esc(label)}" aria-label="${esc(label)}" title="${esc(label)}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.8"></rect><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path></svg></button></div>`;
|
|
236
|
+
}
|
|
146
237
|
function renderItem(item, token) {
|
|
147
238
|
if (item.kind === "system") {
|
|
148
239
|
const parts = [item.title, item.body].filter((x) => Boolean(x)).map(esc);
|
|
@@ -172,42 +263,291 @@ function renderItem(item, token) {
|
|
|
172
263
|
const { username, threadTs, header, content } = parsed;
|
|
173
264
|
const initial = username ? esc(username.slice(0, 2).toUpperCase()) : "U";
|
|
174
265
|
const rawHeader = header ? `<div class="msg-raw-header">${esc(header)}</div>` : "";
|
|
175
|
-
const body = content ?
|
|
266
|
+
const body = content ? renderMarkdownBlock(content, "user") : "";
|
|
176
267
|
const threadBadge = threadTs
|
|
177
268
|
? `<div class="thread-badge" title="Thread ${esc(threadTs)}">Thread · <code>${esc(threadTs)}</code></div>`
|
|
178
269
|
: "";
|
|
179
270
|
const forks = renderForkLinks(item.forks, token ?? "");
|
|
180
|
-
return `<div class="msg-row msg-user">
|
|
181
|
-
<div class="user-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
271
|
+
return `<div class="msg-row msg-user copy-host">
|
|
272
|
+
<div class="msg-main user-main">
|
|
273
|
+
<div class="user-bubble">
|
|
274
|
+
${rawHeader}
|
|
275
|
+
${threadBadge}
|
|
276
|
+
${body}
|
|
277
|
+
${forks}
|
|
278
|
+
${time}
|
|
279
|
+
</div>
|
|
280
|
+
${renderCopyButton()}
|
|
187
281
|
</div>
|
|
188
282
|
<div class="msg-avatar user-avatar" title="${username ? esc(username) : "User"}">${initial}</div>
|
|
189
283
|
</div>`;
|
|
190
284
|
}
|
|
191
285
|
// assistant
|
|
192
|
-
const body = item.body ?
|
|
286
|
+
const body = item.body ? renderMarkdownBlock(item.body, "assistant") : "";
|
|
193
287
|
const forks = renderForkLinks(item.forks, token ?? "");
|
|
194
|
-
return `<div class="msg-row msg-assistant">
|
|
288
|
+
return `<div class="msg-row msg-assistant copy-host">
|
|
195
289
|
<div class="msg-avatar asst-avatar" aria-hidden="true">A</div>
|
|
196
|
-
<div class="asst-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
290
|
+
<div class="msg-main asst-main">
|
|
291
|
+
<div class="asst-card">
|
|
292
|
+
${body}
|
|
293
|
+
${forks}
|
|
294
|
+
${time}
|
|
295
|
+
</div>
|
|
296
|
+
${renderCopyButton()}
|
|
297
|
+
</div>
|
|
298
|
+
</div>`;
|
|
299
|
+
}
|
|
300
|
+
function renderMarkdownBlock(text, variant) {
|
|
301
|
+
return `<div class="msg-body markdown-body markdown-${variant}">${markdown.render(text)}</div>`;
|
|
302
|
+
}
|
|
303
|
+
function renderLiveUserMessage(text, userName) {
|
|
304
|
+
const initial = esc(userName.slice(0, 2).toUpperCase());
|
|
305
|
+
return `<div class="msg-row msg-user copy-host" data-live-item>
|
|
306
|
+
<div class="msg-main user-main">
|
|
307
|
+
<div class="user-bubble">
|
|
308
|
+
${renderMarkdownBlock(text, "user")}
|
|
309
|
+
</div>
|
|
310
|
+
${renderCopyButton()}
|
|
311
|
+
</div>
|
|
312
|
+
<div class="msg-avatar user-avatar" title="${esc(userName)}">${initial}</div>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
function renderLiveAssistantMessage(text) {
|
|
316
|
+
return `<div class="msg-row msg-assistant copy-host" data-live-assistant>
|
|
317
|
+
<div class="msg-avatar asst-avatar" aria-hidden="true">A</div>
|
|
318
|
+
<div class="msg-main asst-main">
|
|
319
|
+
<div class="asst-card">
|
|
320
|
+
${renderMarkdownBlock(text, "assistant")}
|
|
321
|
+
</div>
|
|
322
|
+
${renderCopyButton()}
|
|
200
323
|
</div>
|
|
201
324
|
</div>`;
|
|
202
325
|
}
|
|
326
|
+
function renderLiveToolResult(result) {
|
|
327
|
+
const toneClass = result.isError ? " tone-err" : " tone-ok";
|
|
328
|
+
return `<div class="tool-block" data-live-item>
|
|
329
|
+
<div class="tool-header">
|
|
330
|
+
<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>
|
|
331
|
+
<span class="tool-name">${esc(result.toolName)}</span>
|
|
332
|
+
</div>
|
|
333
|
+
<pre class="tool-output${toneClass}">${esc(result.result)}</pre>
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
function renderLiveSystemEvent(text, tone = "default") {
|
|
337
|
+
const cls = tone === "err" ? " system-event-err" : "";
|
|
338
|
+
return `<div class="system-event${cls}" data-live-item><span class="event-dot"></span><span class="event-text">${esc(text)}</span></div>`;
|
|
339
|
+
}
|
|
340
|
+
async function handleSessionStreamRequest(req, res, url, sessionViewTokenStore, interactive) {
|
|
341
|
+
const token = url.searchParams.get("token")?.trim() ?? "";
|
|
342
|
+
if (!token || !sessionViewTokenStore || !interactive) {
|
|
343
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
344
|
+
res.end("Session stream unavailable");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const entry = sessionViewTokenStore.peek(token);
|
|
348
|
+
if (!entry) {
|
|
349
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
350
|
+
res.end("Invalid session token");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const requestedSession = url.searchParams.get("session");
|
|
354
|
+
const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
|
|
355
|
+
if (!targetSessionFile) {
|
|
356
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
357
|
+
res.end("Invalid session file");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);
|
|
361
|
+
const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });
|
|
362
|
+
res.writeHead(200, {
|
|
363
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
364
|
+
"Cache-Control": "no-store",
|
|
365
|
+
Connection: "keep-alive",
|
|
366
|
+
});
|
|
367
|
+
res.write(`data: ${JSON.stringify({ type: "status", running: interactive.handler.isRunning(activeSessionKey) })}\n\n`);
|
|
368
|
+
const unsubscribe = sessionViewStreamHub.subscribe(streamKey, (event) => {
|
|
369
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
370
|
+
});
|
|
371
|
+
const heartbeat = setInterval(() => {
|
|
372
|
+
res.write(": keep-alive\n\n");
|
|
373
|
+
}, 15000);
|
|
374
|
+
req.on("close", () => {
|
|
375
|
+
clearInterval(heartbeat);
|
|
376
|
+
unsubscribe();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async function handleSessionMessageRequest(req, res, sessionViewTokenStore, interactive) {
|
|
380
|
+
if (!sessionViewTokenStore || !interactive) {
|
|
381
|
+
json(res, 503, { ok: false, error: "Session chat is not configured." });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
let body;
|
|
385
|
+
try {
|
|
386
|
+
body = JSON.parse(await readRequestBody(req));
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
json(res, 400, { ok: false, error: "Invalid request body." });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const token = body.token?.trim() ?? "";
|
|
393
|
+
const text = body.text?.trim() ?? "";
|
|
394
|
+
const requestedSession = body.session?.trim() || null;
|
|
395
|
+
const requestedSessionKey = body.sessionKey?.trim() || "";
|
|
396
|
+
if (!token || !text) {
|
|
397
|
+
json(res, 400, { ok: false, error: "Missing token or text." });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const entry = sessionViewTokenStore.peek(token);
|
|
401
|
+
if (!entry) {
|
|
402
|
+
json(res, 400, { ok: false, error: "This session link is invalid or has expired." });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
|
|
406
|
+
if (!targetSessionFile) {
|
|
407
|
+
json(res, 400, { ok: false, error: "Invalid session file." });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);
|
|
411
|
+
if (requestedSessionKey && requestedSessionKey !== activeSessionKey) {
|
|
412
|
+
json(res, 400, { ok: false, error: "Session target mismatch." });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const bot = interactive.botsByPlatform[entry.platform];
|
|
416
|
+
if (!bot) {
|
|
417
|
+
json(res, 503, { ok: false, error: `No bot configured for ${entry.platform}.` });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });
|
|
421
|
+
const conversationKind = inferConversationKind(entry.platform, entry.conversationId);
|
|
422
|
+
const ts = (Date.now() / 1000).toFixed(6);
|
|
423
|
+
const platformInfo = bot.getPlatformInfo();
|
|
424
|
+
const platformUserName = entry.platformUserName ||
|
|
425
|
+
platformInfo.users.find((user) => user.id === entry.platformUserId)?.userName ||
|
|
426
|
+
platformInfo.users.find((user) => user.id === entry.platformUserId)?.displayName ||
|
|
427
|
+
"unknown";
|
|
428
|
+
const responseCtx = createSessionViewResponseContext((event) => {
|
|
429
|
+
sessionViewStreamHub.publish(streamKey, event);
|
|
430
|
+
});
|
|
431
|
+
const event = {
|
|
432
|
+
type: "session_view",
|
|
433
|
+
conversationId: entry.conversationId,
|
|
434
|
+
conversationKind,
|
|
435
|
+
ts,
|
|
436
|
+
user: entry.platformUserId,
|
|
437
|
+
text,
|
|
438
|
+
attachments: [],
|
|
439
|
+
sessionKey: activeSessionKey,
|
|
440
|
+
...(activeSessionKey.includes(":")
|
|
441
|
+
? { thread_ts: activeSessionKey.split(":").slice(1).join(":") }
|
|
442
|
+
: {}),
|
|
443
|
+
};
|
|
444
|
+
const adapters = {
|
|
445
|
+
message: {
|
|
446
|
+
id: ts,
|
|
447
|
+
sessionKey: activeSessionKey,
|
|
448
|
+
conversationKind,
|
|
449
|
+
userId: entry.platformUserId,
|
|
450
|
+
userName: platformUserName,
|
|
451
|
+
text,
|
|
452
|
+
attachments: [],
|
|
453
|
+
threadTs: event.thread_ts,
|
|
454
|
+
},
|
|
455
|
+
responseCtx,
|
|
456
|
+
platform: { ...platformInfo, diagnostics: { showUsageSummary: false } },
|
|
457
|
+
};
|
|
458
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: true });
|
|
459
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
460
|
+
type: "user",
|
|
461
|
+
html: renderLiveUserMessage(text, platformUserName),
|
|
462
|
+
});
|
|
463
|
+
void interactive.handler
|
|
464
|
+
.handleEvent(event, bot, adapters, false)
|
|
465
|
+
.then(() => {
|
|
466
|
+
if (!targetSessionFile) {
|
|
467
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: false });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const model = loadSessionViewModel(targetSessionFile);
|
|
471
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
472
|
+
type: "refresh",
|
|
473
|
+
timelineHtml: renderTimelineItems(model.items, token),
|
|
474
|
+
updatedAt: formatDate(model.updatedAt),
|
|
475
|
+
entryCount: model.entryCount,
|
|
476
|
+
running: false,
|
|
477
|
+
});
|
|
478
|
+
})
|
|
479
|
+
.catch((error) => {
|
|
480
|
+
log.logWarning(`[${entry.conversationId}] Session view message failed`, error instanceof Error ? error.message : String(error));
|
|
481
|
+
sessionViewStreamHub.publish(streamKey, {
|
|
482
|
+
type: "error",
|
|
483
|
+
message: error instanceof Error ? error.message : String(error),
|
|
484
|
+
});
|
|
485
|
+
sessionViewStreamHub.publish(streamKey, { type: "status", running: false });
|
|
486
|
+
});
|
|
487
|
+
json(res, 202, { ok: true, accepted: true });
|
|
488
|
+
}
|
|
489
|
+
function createSessionViewResponseContext(publish) {
|
|
490
|
+
let accumulatedText = "";
|
|
491
|
+
return {
|
|
492
|
+
respond: async (text) => {
|
|
493
|
+
accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
|
|
494
|
+
publish({ type: "assistant", html: renderLiveAssistantMessage(accumulatedText) });
|
|
495
|
+
},
|
|
496
|
+
replaceResponse: async (text) => {
|
|
497
|
+
accumulatedText = text;
|
|
498
|
+
publish({ type: "assistant", html: renderLiveAssistantMessage(accumulatedText) });
|
|
499
|
+
},
|
|
500
|
+
respondDiagnostic: async (text, options) => {
|
|
501
|
+
if (options?.style === "error") {
|
|
502
|
+
publish({ type: "system", html: renderLiveSystemEvent(text, "err") });
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
respondToolResult: async (result) => {
|
|
506
|
+
publish({ type: "tool", html: renderLiveToolResult(result) });
|
|
507
|
+
},
|
|
508
|
+
setTyping: async () => {
|
|
509
|
+
publish({ type: "status", running: true });
|
|
510
|
+
},
|
|
511
|
+
setWorking: async (working) => {
|
|
512
|
+
publish({ type: "status", running: working });
|
|
513
|
+
},
|
|
514
|
+
uploadFile: async () => { },
|
|
515
|
+
deleteResponse: async () => {
|
|
516
|
+
accumulatedText = "";
|
|
517
|
+
publish({ type: "assistant_remove" });
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function readRequestBody(req) {
|
|
522
|
+
return new Promise((resolve, reject) => {
|
|
523
|
+
let data = "";
|
|
524
|
+
req.setEncoding("utf8");
|
|
525
|
+
req.on("data", (chunk) => {
|
|
526
|
+
data += chunk;
|
|
527
|
+
if (data.length > 1024 * 1024) {
|
|
528
|
+
reject(new Error("Request body too large"));
|
|
529
|
+
req.destroy();
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
req.on("end", () => resolve(data));
|
|
533
|
+
req.on("error", reject);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function json(res, status, body) {
|
|
537
|
+
res.writeHead(status, {
|
|
538
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
539
|
+
"Cache-Control": "no-store",
|
|
540
|
+
});
|
|
541
|
+
res.end(JSON.stringify(body));
|
|
542
|
+
}
|
|
203
543
|
function renderStatusPage(title, message) {
|
|
204
544
|
return renderHtmlDocument(title, `<section class="card stack">
|
|
205
545
|
<p class="eyebrow">mama</p>
|
|
206
546
|
<h1>${esc(title)}</h1>
|
|
207
547
|
<div class="status err">${esc(message)}</div>
|
|
208
|
-
</section
|
|
548
|
+
</section>`, false);
|
|
209
549
|
}
|
|
210
|
-
function renderHtmlDocument(title, shellContent) {
|
|
550
|
+
function renderHtmlDocument(title, shellContent, isRunning) {
|
|
211
551
|
return `<!DOCTYPE html>
|
|
212
552
|
<html lang="en">
|
|
213
553
|
<head>
|
|
@@ -216,10 +556,179 @@ function renderHtmlDocument(title, shellContent) {
|
|
|
216
556
|
<title>${esc(title)}</title>
|
|
217
557
|
<style>${styles}</style>
|
|
218
558
|
</head>
|
|
219
|
-
<body>
|
|
559
|
+
<body data-session-running="${isRunning ? "true" : "false"}">
|
|
220
560
|
<main class="shell">
|
|
221
561
|
${shellContent}
|
|
222
562
|
</main>
|
|
563
|
+
<script>
|
|
564
|
+
const form = document.querySelector('[data-session-composer]');
|
|
565
|
+
const timelineList = document.querySelector('[data-timeline-list]');
|
|
566
|
+
const jumpLatestBtn = document.querySelector('[data-jump-latest]');
|
|
567
|
+
const statusEl = document.querySelector('[data-session-status]');
|
|
568
|
+
const updatedEl = document.querySelector('[data-session-updated]');
|
|
569
|
+
const entriesEl = document.querySelector('[data-session-entries]');
|
|
570
|
+
const composerStatus = form?.querySelector('[data-composer-status]');
|
|
571
|
+
const textarea = form?.querySelector('textarea[name="text"]');
|
|
572
|
+
const submitButton = form?.querySelector('button[type="submit"]');
|
|
573
|
+
let liveAssistant = null;
|
|
574
|
+
let running = document.body.dataset.sessionRunning === 'true';
|
|
575
|
+
|
|
576
|
+
const isNearBottom = () => window.innerHeight + window.scrollY >= document.body.offsetHeight - 120;
|
|
577
|
+
const scrollToLatest = (behavior = 'smooth') => window.scrollTo({ top: document.body.scrollHeight, behavior });
|
|
578
|
+
const toggleJumpButton = () => {
|
|
579
|
+
if (!jumpLatestBtn) return;
|
|
580
|
+
jumpLatestBtn.hidden = isNearBottom();
|
|
581
|
+
};
|
|
582
|
+
const updateFollowState = () => {
|
|
583
|
+
if (isNearBottom()) scrollToLatest('smooth');
|
|
584
|
+
else toggleJumpButton();
|
|
585
|
+
};
|
|
586
|
+
const canSubmit = () => Boolean(textarea && textarea.value.trim()) && !running;
|
|
587
|
+
const updateSubmitButtonState = () => {
|
|
588
|
+
if (submitButton) submitButton.disabled = !canSubmit();
|
|
589
|
+
};
|
|
590
|
+
const setRunning = (value) => {
|
|
591
|
+
running = value;
|
|
592
|
+
document.body.dataset.sessionRunning = value ? 'true' : 'false';
|
|
593
|
+
if (statusEl) statusEl.textContent = value ? 'Running' : 'Idle';
|
|
594
|
+
updateSubmitButtonState();
|
|
595
|
+
if (composerStatus && !value && composerStatus.textContent === 'Thinking…') {
|
|
596
|
+
composerStatus.textContent = '';
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
jumpLatestBtn?.addEventListener('click', () => {
|
|
601
|
+
scrollToLatest('smooth');
|
|
602
|
+
toggleJumpButton();
|
|
603
|
+
});
|
|
604
|
+
document.addEventListener('click', async (event) => {
|
|
605
|
+
const button = event.target instanceof Element ? event.target.closest('[data-copy-button]') : null;
|
|
606
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
607
|
+
const label = button.dataset.copyLabel || 'Copy message';
|
|
608
|
+
const source = button.closest('.msg-actions')?.previousElementSibling;
|
|
609
|
+
const text = source instanceof HTMLElement ? (source.innerText || source.textContent || '').trim() : '';
|
|
610
|
+
if (!text) return;
|
|
611
|
+
const setState = (state, transient) => {
|
|
612
|
+
button.dataset.copyState = state;
|
|
613
|
+
button.title = transient;
|
|
614
|
+
button.setAttribute('aria-label', transient);
|
|
615
|
+
window.setTimeout(() => {
|
|
616
|
+
if (!button.isConnected) return;
|
|
617
|
+
delete button.dataset.copyState;
|
|
618
|
+
button.title = label;
|
|
619
|
+
button.setAttribute('aria-label', label);
|
|
620
|
+
}, 1200);
|
|
621
|
+
};
|
|
622
|
+
try {
|
|
623
|
+
await navigator.clipboard.writeText(text);
|
|
624
|
+
setState('done', 'Copied');
|
|
625
|
+
} catch {
|
|
626
|
+
setState('error', 'Copy failed');
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
window.addEventListener('scroll', toggleJumpButton, { passive: true });
|
|
630
|
+
|
|
631
|
+
if (textarea) {
|
|
632
|
+
const resize = () => {
|
|
633
|
+
textarea.style.height = 'auto';
|
|
634
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 240) + 'px';
|
|
635
|
+
};
|
|
636
|
+
textarea.addEventListener('input', () => {
|
|
637
|
+
resize();
|
|
638
|
+
updateSubmitButtonState();
|
|
639
|
+
});
|
|
640
|
+
textarea.addEventListener('keydown', (event) => {
|
|
641
|
+
if (event.key !== 'Enter' || event.shiftKey) return;
|
|
642
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
643
|
+
event.preventDefault();
|
|
644
|
+
if (!running) form?.requestSubmit();
|
|
645
|
+
});
|
|
646
|
+
resize();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
setRunning(running);
|
|
650
|
+
updateSubmitButtonState();
|
|
651
|
+
|
|
652
|
+
const streamUrl = form
|
|
653
|
+
? '/session/stream?token=' + encodeURIComponent(form.token.value) + '&session=' + encodeURIComponent(form.session.value)
|
|
654
|
+
: null;
|
|
655
|
+
if (streamUrl) {
|
|
656
|
+
const source = new EventSource(streamUrl);
|
|
657
|
+
source.onmessage = (event) => {
|
|
658
|
+
const payload = JSON.parse(event.data);
|
|
659
|
+
switch (payload.type) {
|
|
660
|
+
case 'status':
|
|
661
|
+
setRunning(Boolean(payload.running));
|
|
662
|
+
if (payload.running && composerStatus) composerStatus.textContent = 'Thinking…';
|
|
663
|
+
break;
|
|
664
|
+
case 'user':
|
|
665
|
+
case 'tool':
|
|
666
|
+
case 'system': {
|
|
667
|
+
timelineList?.insertAdjacentHTML('beforeend', payload.html);
|
|
668
|
+
updateFollowState();
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case 'assistant': {
|
|
672
|
+
if (!liveAssistant || !liveAssistant.isConnected) {
|
|
673
|
+
timelineList?.insertAdjacentHTML('beforeend', payload.html);
|
|
674
|
+
liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;
|
|
675
|
+
} else {
|
|
676
|
+
liveAssistant.outerHTML = payload.html;
|
|
677
|
+
liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;
|
|
678
|
+
}
|
|
679
|
+
updateFollowState();
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case 'assistant_remove':
|
|
683
|
+
if (liveAssistant?.isConnected) liveAssistant.remove();
|
|
684
|
+
liveAssistant = null;
|
|
685
|
+
break;
|
|
686
|
+
case 'refresh':
|
|
687
|
+
if (timelineList) timelineList.innerHTML = payload.timelineHtml;
|
|
688
|
+
liveAssistant = null;
|
|
689
|
+
if (updatedEl) updatedEl.textContent = payload.updatedAt;
|
|
690
|
+
if (entriesEl) entriesEl.textContent = String(payload.entryCount);
|
|
691
|
+
setRunning(Boolean(payload.running));
|
|
692
|
+
if (composerStatus) composerStatus.textContent = '';
|
|
693
|
+
updateFollowState();
|
|
694
|
+
break;
|
|
695
|
+
case 'error':
|
|
696
|
+
if (composerStatus) composerStatus.textContent = payload.message || 'Something went wrong';
|
|
697
|
+
setRunning(false);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
form?.addEventListener('submit', async (event) => {
|
|
704
|
+
event.preventDefault();
|
|
705
|
+
if (!textarea || !composerStatus) return;
|
|
706
|
+
const text = textarea.value.trim();
|
|
707
|
+
if (!text || running) return;
|
|
708
|
+
composerStatus.textContent = 'Sending…';
|
|
709
|
+
updateSubmitButtonState();
|
|
710
|
+
try {
|
|
711
|
+
const response = await fetch('/session/message', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: { 'Content-Type': 'application/json' },
|
|
714
|
+
body: JSON.stringify({ token: form.token.value, session: form.session.value, sessionKey: form.sessionKey.value, text }),
|
|
715
|
+
});
|
|
716
|
+
const payload = await response.json();
|
|
717
|
+
if (!response.ok || !payload.ok) throw new Error(payload.error || 'Request failed');
|
|
718
|
+
textarea.value = '';
|
|
719
|
+
textarea.style.height = 'auto';
|
|
720
|
+
composerStatus.textContent = 'Thinking…';
|
|
721
|
+
setRunning(true);
|
|
722
|
+
updateSubmitButtonState();
|
|
723
|
+
scrollToLatest('smooth');
|
|
724
|
+
} catch (err) {
|
|
725
|
+
composerStatus.textContent = err && err.message ? err.message : String(err);
|
|
726
|
+
submitButton.disabled = false;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
toggleJumpButton();
|
|
731
|
+
</script>
|
|
223
732
|
</body>
|
|
224
733
|
</html>`;
|
|
225
734
|
}
|
|
@@ -274,10 +783,11 @@ const styles = `
|
|
|
274
783
|
|
|
275
784
|
body {
|
|
276
785
|
min-height: 100vh;
|
|
277
|
-
padding: 40px 20px
|
|
786
|
+
padding: 40px 20px calc(140px + env(safe-area-inset-bottom, 0px));
|
|
278
787
|
display: flex;
|
|
279
788
|
flex-direction: column;
|
|
280
789
|
align-items: center;
|
|
790
|
+
overflow-x: hidden;
|
|
281
791
|
background-color: var(--bg);
|
|
282
792
|
background-image:
|
|
283
793
|
radial-gradient(ellipse 80% 40% at 50% -10%, rgba(255,255,255,0.6) 0%, transparent 70%);
|
|
@@ -291,6 +801,7 @@ const styles = `
|
|
|
291
801
|
.shell {
|
|
292
802
|
width: 100%;
|
|
293
803
|
max-width: 780px;
|
|
804
|
+
min-width: 0;
|
|
294
805
|
display: flex;
|
|
295
806
|
flex-direction: column;
|
|
296
807
|
gap: 12px;
|
|
@@ -310,8 +821,8 @@ const styles = `
|
|
|
310
821
|
display: flex;
|
|
311
822
|
align-items: flex-start;
|
|
312
823
|
justify-content: space-between;
|
|
313
|
-
gap:
|
|
314
|
-
margin-bottom:
|
|
824
|
+
gap: 20px;
|
|
825
|
+
margin-bottom: 18px;
|
|
315
826
|
}
|
|
316
827
|
|
|
317
828
|
.hero-wordmark {
|
|
@@ -332,61 +843,103 @@ const styles = `
|
|
|
332
843
|
letter-spacing: -0.01em;
|
|
333
844
|
color: var(--text);
|
|
334
845
|
text-wrap: balance;
|
|
846
|
+
margin-bottom: 8px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.hero-meta-line {
|
|
850
|
+
display: flex;
|
|
851
|
+
flex-wrap: wrap;
|
|
852
|
+
gap: 8px 14px;
|
|
853
|
+
color: var(--muted);
|
|
854
|
+
font-size: 0.82rem;
|
|
855
|
+
line-height: 1.4;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.hero-meta-line strong {
|
|
859
|
+
color: var(--text);
|
|
860
|
+
font-weight: 600;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.hero-side {
|
|
864
|
+
display: flex;
|
|
865
|
+
flex-direction: column;
|
|
866
|
+
align-items: flex-end;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
flex-shrink: 0;
|
|
335
869
|
}
|
|
336
870
|
|
|
337
|
-
.
|
|
871
|
+
.hero-badge {
|
|
338
872
|
display: inline-flex;
|
|
339
873
|
align-items: center;
|
|
340
|
-
gap:
|
|
341
|
-
|
|
342
|
-
padding: 7px 14px;
|
|
874
|
+
gap: 8px;
|
|
875
|
+
padding: 6px 11px;
|
|
343
876
|
border: 1px solid var(--border);
|
|
344
877
|
border-radius: 999px;
|
|
345
|
-
background:
|
|
878
|
+
background: rgba(255,255,255,0.7);
|
|
879
|
+
font-size: 0.78rem;
|
|
346
880
|
color: var(--muted);
|
|
347
|
-
|
|
348
|
-
cursor: pointer;
|
|
349
|
-
transition: color 120ms, border-color 120ms, background 120ms;
|
|
350
|
-
white-space: nowrap;
|
|
881
|
+
line-height: 1;
|
|
351
882
|
}
|
|
352
883
|
|
|
353
|
-
.
|
|
884
|
+
.hero-badge strong {
|
|
354
885
|
color: var(--text);
|
|
355
|
-
|
|
356
|
-
|
|
886
|
+
font-weight: 600;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.hero-badge-status.is-running {
|
|
890
|
+
background: #fff7ed;
|
|
891
|
+
border-color: rgba(217, 119, 6, 0.18);
|
|
892
|
+
color: #9a3412;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.hero-badge-dot {
|
|
896
|
+
width: 7px;
|
|
897
|
+
height: 7px;
|
|
898
|
+
border-radius: 50%;
|
|
899
|
+
background: #a1a1aa;
|
|
900
|
+
flex-shrink: 0;
|
|
357
901
|
}
|
|
358
902
|
|
|
359
|
-
.
|
|
360
|
-
|
|
361
|
-
|
|
903
|
+
.hero-badge-status.is-running .hero-badge-dot {
|
|
904
|
+
background: #d97706;
|
|
905
|
+
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.14);
|
|
362
906
|
}
|
|
363
907
|
|
|
364
|
-
.
|
|
908
|
+
.hero-detail-row {
|
|
365
909
|
display: flex;
|
|
366
910
|
flex-wrap: wrap;
|
|
367
|
-
gap:
|
|
911
|
+
gap: 10px;
|
|
912
|
+
padding-top: 14px;
|
|
913
|
+
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
368
914
|
}
|
|
369
915
|
|
|
370
|
-
.
|
|
916
|
+
.hero-detail {
|
|
371
917
|
display: inline-flex;
|
|
372
918
|
align-items: center;
|
|
373
|
-
gap:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
border-radius:
|
|
377
|
-
background:
|
|
378
|
-
|
|
379
|
-
|
|
919
|
+
gap: 8px;
|
|
920
|
+
min-width: 0;
|
|
921
|
+
padding: 6px 10px;
|
|
922
|
+
border-radius: 12px;
|
|
923
|
+
background: rgba(0, 0, 0, 0.025);
|
|
924
|
+
color: var(--muted);
|
|
925
|
+
font-size: 0.78rem;
|
|
380
926
|
}
|
|
381
927
|
|
|
382
|
-
.
|
|
383
|
-
|
|
384
|
-
|
|
928
|
+
.hero-detail-label {
|
|
929
|
+
text-transform: uppercase;
|
|
930
|
+
letter-spacing: 0.08em;
|
|
931
|
+
font-size: 0.68rem;
|
|
932
|
+
color: var(--subtle);
|
|
385
933
|
}
|
|
386
934
|
|
|
387
|
-
.
|
|
935
|
+
.hero-detail code {
|
|
936
|
+
min-width: 0;
|
|
937
|
+
overflow: hidden;
|
|
938
|
+
text-overflow: ellipsis;
|
|
939
|
+
white-space: nowrap;
|
|
940
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
941
|
+
font-size: 0.74rem;
|
|
388
942
|
color: var(--text);
|
|
389
|
-
font-weight: 600;
|
|
390
943
|
}
|
|
391
944
|
|
|
392
945
|
/* ── Timeline shell ───────────────────────────────────────────────────── */
|
|
@@ -507,7 +1060,100 @@ const styles = `
|
|
|
507
1060
|
.timeline-list {
|
|
508
1061
|
display: flex;
|
|
509
1062
|
flex-direction: column;
|
|
510
|
-
gap:
|
|
1063
|
+
gap: 14px;
|
|
1064
|
+
min-width: 0;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.copy-host {
|
|
1068
|
+
position: relative;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.msg-actions {
|
|
1072
|
+
height: 32px;
|
|
1073
|
+
display: flex;
|
|
1074
|
+
align-items: center;
|
|
1075
|
+
gap: 8px;
|
|
1076
|
+
margin-top: 8px;
|
|
1077
|
+
opacity: 0;
|
|
1078
|
+
visibility: hidden;
|
|
1079
|
+
transition: opacity 140ms ease, visibility 140ms ease;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.copy-host:hover .msg-actions,
|
|
1083
|
+
.copy-host .msg-actions:hover,
|
|
1084
|
+
.copy-host:focus-within .msg-actions,
|
|
1085
|
+
.timeline-list > .copy-host:last-child .msg-actions,
|
|
1086
|
+
.copy-action-btn[data-copy-state] {
|
|
1087
|
+
opacity: 1;
|
|
1088
|
+
visibility: visible;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.copy-action-btn {
|
|
1092
|
+
width: 24px;
|
|
1093
|
+
height: 24px;
|
|
1094
|
+
display: inline-flex;
|
|
1095
|
+
align-items: center;
|
|
1096
|
+
justify-content: center;
|
|
1097
|
+
border: 0;
|
|
1098
|
+
border-radius: 0;
|
|
1099
|
+
background: transparent;
|
|
1100
|
+
color: rgba(63,63,70,0.8);
|
|
1101
|
+
transition: color 140ms ease, opacity 140ms ease;
|
|
1102
|
+
cursor: pointer;
|
|
1103
|
+
padding: 0;
|
|
1104
|
+
appearance: none;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.copy-action-btn:hover {
|
|
1108
|
+
background: transparent;
|
|
1109
|
+
color: rgba(24,24,27,0.96);
|
|
1110
|
+
border-color: transparent;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.copy-action-btn[data-copy-state='done'] {
|
|
1114
|
+
background: transparent;
|
|
1115
|
+
border-color: transparent;
|
|
1116
|
+
color: rgba(24,24,27,0.96);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.copy-action-btn[data-copy-state='done'] svg {
|
|
1120
|
+
position: absolute;
|
|
1121
|
+
opacity: 0;
|
|
1122
|
+
transform: scale(0.6);
|
|
1123
|
+
pointer-events: none;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.copy-action-btn svg {
|
|
1127
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.copy-action-btn[data-copy-state='done']::before {
|
|
1131
|
+
content: '';
|
|
1132
|
+
width: 14px;
|
|
1133
|
+
height: 14px;
|
|
1134
|
+
background-color: currentColor;
|
|
1135
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>") center / contain no-repeat;
|
|
1136
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>") center / contain no-repeat;
|
|
1137
|
+
animation: copy-check-in 200ms ease-out both;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
@keyframes copy-check-in {
|
|
1141
|
+
from { opacity: 0; transform: scale(0.6); }
|
|
1142
|
+
to { opacity: 1; transform: scale(1); }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1146
|
+
.copy-action-btn svg,
|
|
1147
|
+
.copy-action-btn[data-copy-state='done']::before {
|
|
1148
|
+
transition: none;
|
|
1149
|
+
animation: none;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.copy-action-btn[data-copy-state='error'] {
|
|
1154
|
+
background: transparent;
|
|
1155
|
+
border-color: transparent;
|
|
1156
|
+
color: #b91c1c;
|
|
511
1157
|
}
|
|
512
1158
|
|
|
513
1159
|
/* ── Message rows ─────────────────────────────────────────────────────── */
|
|
@@ -516,7 +1162,8 @@ const styles = `
|
|
|
516
1162
|
display: flex;
|
|
517
1163
|
align-items: flex-end;
|
|
518
1164
|
gap: 8px;
|
|
519
|
-
padding:
|
|
1165
|
+
padding: 4px 0;
|
|
1166
|
+
min-width: 0;
|
|
520
1167
|
}
|
|
521
1168
|
|
|
522
1169
|
/* ── User messages ────────────────────────────────────────────────────── */
|
|
@@ -525,8 +1172,20 @@ const styles = `
|
|
|
525
1172
|
justify-content: flex-end;
|
|
526
1173
|
}
|
|
527
1174
|
|
|
528
|
-
.
|
|
1175
|
+
.msg-main {
|
|
1176
|
+
min-width: 0;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.user-main {
|
|
529
1180
|
max-width: 85%;
|
|
1181
|
+
display: flex;
|
|
1182
|
+
flex-direction: column;
|
|
1183
|
+
align-items: flex-end;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.user-bubble {
|
|
1187
|
+
max-width: 100%;
|
|
1188
|
+
min-width: 0;
|
|
530
1189
|
padding: 12px 16px;
|
|
531
1190
|
border-radius: 18px 18px 4px 18px;
|
|
532
1191
|
background: var(--user-bg);
|
|
@@ -568,11 +1227,6 @@ const styles = `
|
|
|
568
1227
|
}
|
|
569
1228
|
|
|
570
1229
|
.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
1230
|
color: var(--user-text);
|
|
577
1231
|
}
|
|
578
1232
|
|
|
@@ -619,10 +1273,19 @@ const styles = `
|
|
|
619
1273
|
align-items: flex-end;
|
|
620
1274
|
gap: 8px;
|
|
621
1275
|
max-width: 85%;
|
|
1276
|
+
min-width: 0;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.asst-main {
|
|
1280
|
+
max-width: 100%;
|
|
1281
|
+
display: flex;
|
|
1282
|
+
flex-direction: column;
|
|
1283
|
+
align-items: flex-start;
|
|
622
1284
|
}
|
|
623
1285
|
|
|
624
1286
|
.asst-card {
|
|
625
1287
|
min-width: 0;
|
|
1288
|
+
max-width: 100%;
|
|
626
1289
|
padding: 14px 18px;
|
|
627
1290
|
border: 1px solid var(--border);
|
|
628
1291
|
border-radius: 18px 18px 18px 4px;
|
|
@@ -631,11 +1294,6 @@ const styles = `
|
|
|
631
1294
|
}
|
|
632
1295
|
|
|
633
1296
|
.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
1297
|
color: var(--text);
|
|
640
1298
|
}
|
|
641
1299
|
|
|
@@ -655,7 +1313,7 @@ const styles = `
|
|
|
655
1313
|
overflow: hidden;
|
|
656
1314
|
border: 1px solid rgba(255,255,255,0.06);
|
|
657
1315
|
box-shadow: 0 2px 8px rgba(0,0,0,0.16);
|
|
658
|
-
margin:
|
|
1316
|
+
margin: 2px 0;
|
|
659
1317
|
}
|
|
660
1318
|
|
|
661
1319
|
.tool-header {
|
|
@@ -711,6 +1369,143 @@ const styles = `
|
|
|
711
1369
|
.tool-output.tone-ok { color: var(--tool-ok); }
|
|
712
1370
|
.tool-output.tone-err { color: var(--tool-err); }
|
|
713
1371
|
|
|
1372
|
+
/* ── Markdown blocks ──────────────────────────────────────────────────── */
|
|
1373
|
+
|
|
1374
|
+
.markdown-body {
|
|
1375
|
+
font-family: 'DM Sans', system-ui, sans-serif;
|
|
1376
|
+
font-size: 0.9rem;
|
|
1377
|
+
line-height: 1.65;
|
|
1378
|
+
word-break: break-word;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.markdown-body > *:first-child { margin-top: 0; }
|
|
1382
|
+
.markdown-body > *:last-child { margin-bottom: 0; }
|
|
1383
|
+
.markdown-body p,
|
|
1384
|
+
.markdown-body ul,
|
|
1385
|
+
.markdown-body ol,
|
|
1386
|
+
.markdown-body blockquote,
|
|
1387
|
+
.markdown-body pre,
|
|
1388
|
+
.markdown-body table,
|
|
1389
|
+
.markdown-body hr {
|
|
1390
|
+
margin: 0 0 0.85em;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.markdown-body h1,
|
|
1394
|
+
.markdown-body h2,
|
|
1395
|
+
.markdown-body h3,
|
|
1396
|
+
.markdown-body h4,
|
|
1397
|
+
.markdown-body h5,
|
|
1398
|
+
.markdown-body h6 {
|
|
1399
|
+
margin: 0 0 0.55em;
|
|
1400
|
+
line-height: 1.25;
|
|
1401
|
+
font-weight: 700;
|
|
1402
|
+
letter-spacing: -0.01em;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.markdown-body h1 { font-size: 1.4rem; }
|
|
1406
|
+
.markdown-body h2 { font-size: 1.22rem; }
|
|
1407
|
+
.markdown-body h3 { font-size: 1.08rem; }
|
|
1408
|
+
.markdown-body h4,
|
|
1409
|
+
.markdown-body h5,
|
|
1410
|
+
.markdown-body h6 { font-size: 0.95rem; }
|
|
1411
|
+
|
|
1412
|
+
.markdown-body ul,
|
|
1413
|
+
.markdown-body ol {
|
|
1414
|
+
padding-left: 1.3em;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.markdown-body li + li {
|
|
1418
|
+
margin-top: 0.22em;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.markdown-body blockquote {
|
|
1422
|
+
padding-left: 12px;
|
|
1423
|
+
border-left: 3px solid rgba(34, 197, 94, 0.35);
|
|
1424
|
+
opacity: 0.95;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
.markdown-body a {
|
|
1428
|
+
color: inherit;
|
|
1429
|
+
text-decoration: underline;
|
|
1430
|
+
text-underline-offset: 2px;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.markdown-body code {
|
|
1434
|
+
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
|
1435
|
+
font-size: 0.82em;
|
|
1436
|
+
padding: 0.16em 0.38em;
|
|
1437
|
+
border-radius: 6px;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.markdown-body pre {
|
|
1441
|
+
overflow-x: auto;
|
|
1442
|
+
border-radius: 12px;
|
|
1443
|
+
padding: 12px 14px;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.markdown-body pre code {
|
|
1447
|
+
display: block;
|
|
1448
|
+
padding: 0;
|
|
1449
|
+
border-radius: 0;
|
|
1450
|
+
background: transparent;
|
|
1451
|
+
font-size: 0.82rem;
|
|
1452
|
+
line-height: 1.6;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.markdown-body table {
|
|
1456
|
+
width: 100%;
|
|
1457
|
+
border-collapse: collapse;
|
|
1458
|
+
font-size: 0.85rem;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.markdown-body th,
|
|
1462
|
+
.markdown-body td {
|
|
1463
|
+
padding: 8px 10px;
|
|
1464
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
1465
|
+
text-align: left;
|
|
1466
|
+
vertical-align: top;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
.markdown-body img {
|
|
1470
|
+
max-width: 100%;
|
|
1471
|
+
border-radius: 12px;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
.markdown-user code {
|
|
1475
|
+
background: rgba(255,255,255,0.14);
|
|
1476
|
+
color: var(--user-text);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.markdown-user pre {
|
|
1480
|
+
background: rgba(255,255,255,0.08);
|
|
1481
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
.markdown-user table th,
|
|
1485
|
+
.markdown-user table td {
|
|
1486
|
+
border-color: rgba(255,255,255,0.16);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.markdown-assistant code {
|
|
1490
|
+
background: #f4f4f5;
|
|
1491
|
+
color: #27272a;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
.markdown-assistant pre {
|
|
1495
|
+
background: #0f172a;
|
|
1496
|
+
color: #e5e7eb;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.markdown-assistant pre code {
|
|
1500
|
+
background: transparent;
|
|
1501
|
+
color: inherit;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.markdown-assistant table th,
|
|
1505
|
+
.markdown-assistant table td {
|
|
1506
|
+
border-color: rgba(0, 0, 0, 0.08);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
714
1509
|
/* ── System events ────────────────────────────────────────────────────── */
|
|
715
1510
|
|
|
716
1511
|
.system-event {
|
|
@@ -736,6 +1531,10 @@ const styles = `
|
|
|
736
1531
|
color: var(--muted);
|
|
737
1532
|
}
|
|
738
1533
|
|
|
1534
|
+
.system-event-err .event-text {
|
|
1535
|
+
color: var(--err-text);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
739
1538
|
.event-time {
|
|
740
1539
|
color: var(--subtle);
|
|
741
1540
|
font-style: normal;
|
|
@@ -783,18 +1582,157 @@ const styles = `
|
|
|
783
1582
|
border: 1px solid rgba(185, 28, 28, 0.12);
|
|
784
1583
|
}
|
|
785
1584
|
|
|
1585
|
+
/* ── Composer ─────────────────────────────────────────────────────────── */
|
|
1586
|
+
|
|
1587
|
+
.composer-card {
|
|
1588
|
+
position: fixed;
|
|
1589
|
+
left: 50%;
|
|
1590
|
+
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
1591
|
+
transform: translateX(-50%);
|
|
1592
|
+
width: calc(100% - 32px);
|
|
1593
|
+
max-width: 780px;
|
|
1594
|
+
padding: 10px 12px 10px 14px;
|
|
1595
|
+
border: 1px solid var(--border);
|
|
1596
|
+
border-radius: 22px;
|
|
1597
|
+
background: rgba(250, 248, 244, 0.92);
|
|
1598
|
+
box-shadow: 0 12px 36px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.04);
|
|
1599
|
+
backdrop-filter: blur(14px);
|
|
1600
|
+
-webkit-backdrop-filter: blur(14px);
|
|
1601
|
+
z-index: 20;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.composer-card .composer-copy { display: none; }
|
|
1605
|
+
|
|
1606
|
+
.composer-form {
|
|
1607
|
+
display: flex;
|
|
1608
|
+
flex-direction: column;
|
|
1609
|
+
gap: 6px;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
.jump-latest-btn {
|
|
1613
|
+
position: fixed;
|
|
1614
|
+
left: 50%;
|
|
1615
|
+
bottom: calc(env(safe-area-inset-bottom, 0px) + 120px);
|
|
1616
|
+
z-index: 25;
|
|
1617
|
+
width: 42px;
|
|
1618
|
+
height: 42px;
|
|
1619
|
+
border: 1px solid var(--border);
|
|
1620
|
+
border-radius: 999px;
|
|
1621
|
+
background: var(--bg);
|
|
1622
|
+
color: var(--text);
|
|
1623
|
+
font: 700 1rem/1 'DM Sans', sans-serif;
|
|
1624
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
|
|
1625
|
+
cursor: pointer;
|
|
1626
|
+
backdrop-filter: blur(10px);
|
|
1627
|
+
transform: translateX(-50%);
|
|
1628
|
+
outline: none;
|
|
1629
|
+
appearance: none;
|
|
1630
|
+
-webkit-tap-highlight-color: transparent;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
.jump-latest-btn:hover {
|
|
1634
|
+
transform: translateX(-50%) translateY(-1px);
|
|
1635
|
+
background: #e8e3d9;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
.jump-latest-btn:focus,
|
|
1639
|
+
.jump-latest-btn:active {
|
|
1640
|
+
outline: none;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
.jump-latest-btn:focus-visible {
|
|
1644
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.12), 0 0 0 3px rgba(0,0,0,0.08);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
.composer-copy { margin-bottom: 12px; color: var(--muted); }
|
|
1648
|
+
|
|
1649
|
+
.composer-form textarea {
|
|
1650
|
+
width: 100%;
|
|
1651
|
+
resize: none;
|
|
1652
|
+
overflow-y: auto;
|
|
1653
|
+
min-height: 28px;
|
|
1654
|
+
max-height: 200px;
|
|
1655
|
+
padding: 6px 6px 2px;
|
|
1656
|
+
border: 0;
|
|
1657
|
+
border-radius: 0;
|
|
1658
|
+
font: inherit;
|
|
1659
|
+
color: var(--text);
|
|
1660
|
+
background: transparent;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
.composer-form textarea::placeholder {
|
|
1664
|
+
color: rgba(63,63,70,0.55);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.composer-form textarea:focus {
|
|
1668
|
+
outline: none;
|
|
1669
|
+
border: 0;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.composer-actions {
|
|
1673
|
+
display: flex;
|
|
1674
|
+
align-items: center;
|
|
1675
|
+
justify-content: space-between;
|
|
1676
|
+
gap: 12px;
|
|
1677
|
+
margin-top: 0;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
.composer-status { color: var(--muted); font-size: 13px; }
|
|
1681
|
+
.composer-actions button:disabled { opacity: 0.55; cursor: wait; }
|
|
1682
|
+
|
|
1683
|
+
.composer-send-btn {
|
|
1684
|
+
display: inline-flex;
|
|
1685
|
+
align-items: center;
|
|
1686
|
+
justify-content: center;
|
|
1687
|
+
width: 32px;
|
|
1688
|
+
height: 32px;
|
|
1689
|
+
border: none;
|
|
1690
|
+
border-radius: 999px;
|
|
1691
|
+
background: #d97706;
|
|
1692
|
+
color: #ffffff;
|
|
1693
|
+
font: 700 1rem/1 'DM Sans', sans-serif;
|
|
1694
|
+
cursor: pointer;
|
|
1695
|
+
box-shadow: 0 10px 24px rgba(217, 119, 6, 0.26);
|
|
1696
|
+
transition: transform 120ms, filter 120ms, box-shadow 120ms, background 120ms;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
.composer-send-btn:hover:not(:disabled) {
|
|
1700
|
+
transform: translateY(-1px);
|
|
1701
|
+
filter: saturate(1.06) brightness(0.98);
|
|
1702
|
+
box-shadow: 0 12px 28px rgba(217, 119, 6, 0.32);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.composer-send-btn:focus-visible {
|
|
1706
|
+
outline: 2px solid rgba(217, 119, 6, 0.28);
|
|
1707
|
+
outline-offset: 3px;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.composer-send-btn:disabled {
|
|
1711
|
+
background: #d4d4d8;
|
|
1712
|
+
color: rgba(24, 24, 27, 0.45);
|
|
1713
|
+
box-shadow: none;
|
|
1714
|
+
transform: none;
|
|
1715
|
+
filter: none;
|
|
1716
|
+
cursor: not-allowed;
|
|
1717
|
+
opacity: 1;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
786
1720
|
/* ── Responsive ───────────────────────────────────────────────────────── */
|
|
787
1721
|
|
|
788
1722
|
@media (max-width: 600px) {
|
|
789
|
-
body { padding: 20px 12px
|
|
1723
|
+
body { padding: 20px 12px calc(130px + env(safe-area-inset-bottom, 0px)); }
|
|
1724
|
+
|
|
1725
|
+
.composer-card { width: calc(100% - 16px); bottom: calc(8px + env(safe-area-inset-bottom, 0px)); padding: 8px 10px; border-radius: 18px; }
|
|
790
1726
|
|
|
791
1727
|
.hero-card, .card { padding: 20px; border-radius: 16px; }
|
|
792
1728
|
|
|
793
1729
|
.hero-top { flex-direction: column; gap: 12px; }
|
|
1730
|
+
.hero-side { align-items: flex-start; }
|
|
1731
|
+
.hero-detail-row { gap: 8px; }
|
|
794
1732
|
|
|
795
|
-
.
|
|
796
|
-
|
|
797
|
-
.
|
|
1733
|
+
.user-bubble,
|
|
1734
|
+
.msg-assistant,
|
|
1735
|
+
.tool-block { max-width: 100%; }
|
|
798
1736
|
|
|
799
1737
|
.asst-avatar { display: none; }
|
|
800
1738
|
|