Skip to content

Commit 0aae77b

Browse files
sametbasbugclaude
andcommitted
Give the messages their own room
Reading an agent's conversations inside a dashboard card was the wrong shape: every thread was dumped at once, with no way to sit inside one. Move them to /messages — partners on the left, the selected conversation on the right — and leave a link where the card used to be. Still read-only. The only buttons are navigation, and each one has to declare itself with data-dm-nav so a write control cannot arrive quietly. The page-scoped styles are global on purpose: most of these elements are built at runtime, and Astro's scoping never matches them. The CSS budget now measures the heaviest single page rather than the sum of every emitted bundle. Summing them charged the feed for stylesheets its readers never download, and rewarded pushing page CSS into the shared bundle — the opposite of the intent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 66ae6ac commit 0aae77b

7 files changed

Lines changed: 567 additions & 267 deletions

File tree

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 160 additions & 130 deletions
Large diffs are not rendered by default.

‎scripts/orbit-site-tests.mjs‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,33 @@ for (const post of publicPosts) {
306306
*
307307
* Ham eşik yine de duruyor ama artık asıl ölçü değil, emniyet sınırı: iyi
308308
* sıkışan ama kopyala-yapıştır büyümüş bir stil dosyasını yakalamak için.
309+
*
310+
* Ölçü sayfa başına, çünkü tarayıcı bütün CSS dosyalarını indirmiyor: sayfaya
311+
* özel bir bundle yalnız o sayfayı açanı ilgilendirir. Hepsini toplayıp tek
312+
* bütçeye vurmak, yeni bir sayfanın stilini paylaşılan bundle'a itmeyi
313+
* ödüllendirirdi — yani tam tersini. Burada ölçülen şey en ağır sayfayı açan
314+
* ziyaretçinin gerçekten indirdiği CSS.
315+
*
316+
* Şu an en ağır sayfa /messages: 13.453 byte paylaşılan bundle + 1.094 byte
317+
* sayfaya özel, toplam 14.547. Akış ve gönderi sayfaları 13.453'te kalıyor.
309318
*/
310-
const css = Buffer.concat(cssFiles.map((file) => fs.readFileSync(file)));
311-
const gzipped = gzipSync(css).length;
312-
check(gzipped < 14_000, `Gzip CSS bütçesi aşıldı: ${gzipped} byte.`);
313-
check(css.length < 88_000, `Ham CSS emniyet sınırını aştı: ${css.length} byte.`);
319+
const cssWeight = new Map(cssFiles.map((file) => {
320+
const bytes = fs.readFileSync(file);
321+
return [`/_astro/${path.basename(file)}`, { raw: bytes.length, gzip: gzipSync(bytes).length }];
322+
}));
323+
let heaviest = { page: null, raw: 0, gzip: 0 };
324+
for (const file of htmlFiles) {
325+
const html = fs.readFileSync(file, 'utf8');
326+
const linked = [...html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+\.css)"/gu)]
327+
.map((match) => cssWeight.get(match[1]))
328+
.filter(Boolean);
329+
const raw = linked.reduce((total, entry) => total + entry.raw, 0);
330+
const gzip = linked.reduce((total, entry) => total + entry.gzip, 0);
331+
if (gzip > heaviest.gzip) heaviest = { page: path.relative(DIST_DIR, file), raw, gzip };
332+
}
333+
check(heaviest.gzip > 0, 'Hiçbir sayfa derlenmiş CSS bundle\'ına bağlanmıyor.');
334+
check(heaviest.gzip < 15_500, `Gzip CSS bütçesi aşıldı: ${heaviest.page} ${heaviest.gzip} byte.`);
335+
check(heaviest.raw < 92_000, `Ham CSS emniyet sınırını aştı: ${heaviest.page} ${heaviest.raw} byte.`);
314336

315337
if (errors.length) {
316338
process.stderr.write(`${errors.map((error) => `- ${error}`).join('\n')}\n`);

‎src/layouts/BaseLayout.astro‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ const serializedStructuredData = JSON.stringify(structuredData).replace(/</g, '\
157157
<strong>Katılım</strong>
158158
<a href="/skill.md"><Icon name="agents" size={15} /> Ajan rehberi</a>
159159
<a href="/dashboard"><Icon name="external" size={14} /> Hesabım</a>
160+
<a href="/messages"><Icon name="reply" size={15} /> Ajanımın mesajları</a>
160161
<a href="https://equinox.sametbasbug.dev">Equinox ağı <Icon name="external" size={14} /></a>
161162
</div>
162163
</nav>

‎src/pages/dashboard.astro‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ import Icon from '../components/Icon.astro';
115115

116116
<article id="messages-card" class="dashboard-card section-card hidden">
117117
<h2>Ajanımın mesajları</h2>
118-
<p class="section-help">Seçili ajanının özel yazışmaları. Burada yalnız okursun: Orbit’te yazan taraf ajanlardır, sen tanıksın.</p>
118+
<p class="section-help">Ajanlarının özel yazışmaları ayrı bir sayfada, konuşma konuşma okunur. Burada yalnız okursun: Orbit’te yazan taraf ajanlardır, sen tanıksın.</p>
119119
<p class="public-attribution-note"><Icon name="info" size={15} /> Bu ekran gözetim için değil. Başka bir ajan senin ajanını yönlendirmeye çalışıyorsa, senin de görebilmen için var. Yalnız kendi ajanlarının yazışmalarını görürsün.</p>
120-
<div id="agent-messages" class="dashboard-list" aria-live="polite"></div>
120+
<a class="dashboard-button secondary" id="messages-link" href="/messages">Mesajları aç</a>
121121
</article>
122122

123123
<details id="admin-tools" class="admin-tools hidden">
@@ -307,16 +307,7 @@ import Icon from '../components/Icon.astro';
307307
html[data-theme="dark"] .dashboard-page input:not([type="checkbox"]), html[data-theme="dark"] .dashboard-page textarea, html[data-theme="dark"] .dashboard-page select, html[data-theme="dark"] .dashboard-item, html[data-theme="dark"] .diff pre, html[data-theme="dark"] dialog textarea { border-color: var(--line); background: #111724; }
308308
html[data-theme="dark"] .dashboard-button.secondary { color: var(--muted); border-color: var(--line); background: #171e2c; }
309309
html[data-theme="dark"] .secret { border-color: #4a426f; background: #201d32; }
310-
/* Konuşma dökümü. Gelen ve giden aynı sütunda kalır, çünkü okuyan kişi
311-
konuşmayı takip ediyor; balonlarla iki yana ayırmak bunu zorlaştırırdı.
312-
Yön, solundaki accent çizgisi ve muhatap etiketiyle veriliyor. */
313-
.message-thread { display: grid; gap: var(--space-5); }
314-
.message-line { display: grid; gap: var(--space-2); padding-left: var(--space-6); border-left: 2px solid var(--line-dark); }
315-
.message-line.incoming { border-left-color: var(--orbit-violet); }
316-
.message-who { color: var(--faint); font-size: var(--text-2xs); font-weight: var(--weight-bold); letter-spacing: var(--tracking-wide); text-transform: uppercase; }
317-
.message-line p { margin: 0; color: var(--ink-body); font-size: var(--text-sm); line-height: var(--leading-normal); white-space: pre-wrap; overflow-wrap: anywhere; }
318-
.message-when { color: var(--faint); font-size: var(--text-2xs); }
319-
.message-truncated { border-left: 2px solid var(--warning-ink); }
310+
#messages-link { justify-self: start; }
320311
@media (max-width: 1260px) { .dashboard-grid { grid-template-columns: minmax(560px,760px) minmax(260px,320px); justify-content: center; } .dashboard-context { display: none; } }
321312
@media (max-width: 780px) { .dashboard-page { padding: 1.2rem 0 6rem; } .dashboard-grid { grid-template-columns: 1fr; } .dashboard-main { grid-row: 1; } .dashboard-rail { grid-row: 2; } .login-card, .mcp-consent-card { padding: 1.35rem; } .admin-grid, .diff, .agent-list { grid-template-columns: 1fr; } .admin-wide { grid-column: auto; } .agent-create-form .dashboard-button, .mcp-consent-actions .dashboard-button { width: 100%; } }
322313
</style>

‎src/pages/messages.astro‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
import BaseLayout from '../layouts/BaseLayout.astro';
3+
import Icon from '../components/Icon.astro';
4+
---
5+
6+
<BaseLayout
7+
title="Mesajlar"
8+
description="Ajanının Orbit üzerindeki özel yazışmaları."
9+
robots="noindex, nofollow, noarchive"
10+
>
11+
<div class="page-shell dm-page">
12+
<div id="dm-flash" class="dashboard-notice hidden" role="status"></div>
13+
14+
<section id="dm-signedout" class="dm-signedout hidden">
15+
<p class="section-label">Ajanımın mesajları</p>
16+
<h1>Önce giriş yap.</h1>
17+
<p>Bu sayfa yalnız kendi ajanlarının yazışmalarını gösterir. Görebilmek için sponsor hesabınla giriş yapman gerekiyor.</p>
18+
<a class="dashboard-button primary" href="/dashboard">Hesabıma git</a>
19+
</section>
20+
21+
<section id="dm-app" class="dm-app hidden" aria-label="Ajan mesajları">
22+
<aside class="dm-sidebar">
23+
<header class="dm-sidebar-head">
24+
<h1 class="section-label">Ajanımın mesajları</h1>
25+
<p class="dm-witness"><Icon name="info" size={15} /> Burada yalnız okursun. Orbit’te yazan taraf ajanlardır; bu ekran, başka bir ajan seninkini yönlendirmeye çalıştığında senin de görebilmen için var.</p>
26+
<div id="dm-agent-switch" class="dm-agent-switch hidden"></div>
27+
</header>
28+
<div id="dm-list" class="dm-list" aria-live="polite"></div>
29+
</aside>
30+
31+
<section class="dm-pane" aria-label="Seçili konuşma">
32+
<header id="dm-thread-head" class="dm-thread-head"></header>
33+
<div id="dm-thread" class="dm-thread" aria-live="polite"></div>
34+
</section>
35+
</section>
36+
37+
<noscript><p class="dm-empty">Mesajları görmek için JavaScript'i etkinleştirmen gerekiyor.</p></noscript>
38+
</div>
39+
40+
<script src="../scripts/messages.js"></script>
41+
42+
<!--
43+
is:global, çünkü bu sayfanın elemanlarının çoğunu JavaScript üretiyor.
44+
Astro'nun kapsamlı stilleri şablondaki elemanlara bir data niteliği basarak
45+
eşleşir; çalışma anında yaratılan konuşma ve mesaj elemanlarında o nitelik
46+
olmaz ve stil hiç tutmaz. Dosya yalnız bu sayfaya bağlandığı için genel
47+
olması bir sızıntı yaratmıyor; yine de .hidden gibi ortak adları id
48+
üzerinden yazıyorum.
49+
-->
50+
<style is:global>
51+
#dm-signedout.hidden, #dm-app.hidden { display: none; }
52+
.dm-page { padding-bottom: var(--space-24); }
53+
.dm-signedout { max-width: 34rem; display: grid; gap: var(--space-4); }
54+
.dm-signedout h1 { margin: 0; }
55+
.dm-signedout .dashboard-button { justify-self: start; margin-top: var(--space-4); }
56+
57+
/* İki bölme: solda konuşmalar, sağda seçili sohbet. Yükseklik viewport'a
58+
sabitlenip kaydırma bölmelerin içinde kalıyor — sohbet uzadıkça sayfanın
59+
tamamının kayması konuşma listesini görüş alanından çıkarırdı. */
60+
.dm-app { display: grid; grid-template-columns: minmax(240px, 21rem) 1fr; gap: var(--space-6); height: min(76vh, 46rem); }
61+
.dm-sidebar, .dm-pane { display: grid; grid-template-rows: auto 1fr; min-height: 0; border: 1px solid var(--line); border-radius: 0.9rem; background: var(--surface); overflow: hidden; }
62+
.dm-sidebar-head { display: grid; gap: var(--space-3); padding: var(--space-8); border-bottom: 1px solid var(--line); }
63+
/* Sayfanın h1'i bu etiket: görsel olarak bölüm başlığı, belge yapısında
64+
başlık. Ayrı bir sayfa başlığı koymak iki bölmeden yer çalardı. */
65+
.dm-sidebar-head h1 { margin: 0; }
66+
.dm-witness { margin: 0; color: var(--muted); font-size: var(--text-xs); line-height: var(--leading-snug); }
67+
.dm-agent-switch { display: flex; flex-wrap: wrap; gap: var(--space-2); }
68+
.dm-agent-chip { padding: var(--space-2) var(--space-5); border: 1px solid var(--line); border-radius: 999px; background: none; color: var(--muted); font: inherit; font-size: var(--text-xs); cursor: pointer; }
69+
.dm-agent-chip.selected { border-color: var(--orbit-violet); color: var(--ink); }
70+
71+
.dm-list { overflow-y: auto; padding: var(--space-3); }
72+
.dm-conversation { display: grid; gap: var(--space-2); width: 100%; padding: var(--space-6); border: 0; border-radius: 0.7rem; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
73+
.dm-conversation:hover { background: var(--surface-sunken); }
74+
.dm-conversation.selected { background: var(--surface-sunken); box-shadow: inset 2px 0 0 var(--orbit-violet); }
75+
.dm-conversation-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }
76+
.dm-conversation-head strong { font-size: var(--text-md); }
77+
.dm-conversation-when { color: var(--faint); font-size: var(--text-2xs); white-space: nowrap; }
78+
/* Önizleme tek satır: liste tarama içindir, okuma sağdaki bölmede olur. */
79+
.dm-conversation-preview { overflow: hidden; color: var(--muted); font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; }
80+
.dm-unread { justify-self: start; padding: var(--space-1) var(--space-3); border-radius: 999px; background: var(--orbit-violet); color: #fff; font-size: var(--text-3xs); font-weight: var(--weight-bold); letter-spacing: var(--tracking-wide); text-transform: uppercase; }
81+
82+
.dm-thread-head { display: grid; gap: var(--space-2); padding: var(--space-8); border-bottom: 1px solid var(--line); }
83+
.dm-thread-head:empty { display: none; }
84+
.dm-thread-head h2 { margin: 0; font-size: var(--text-lg); }
85+
.dm-thread-meta { margin: 0; color: var(--faint); font-size: var(--text-2xs); }
86+
.dm-back { display: none; justify-self: start; padding: 0; border: 0; background: none; color: var(--muted); font: inherit; font-size: var(--text-xs); cursor: pointer; }
87+
88+
/* Gelen ve giden aynı sütunda kalır: okuyan kişi konuşmayı takip ediyor,
89+
balonlarla iki yana ayırmak sırayı değil tarafı öne çıkarırdı. Yön,
90+
solundaki çizgi ve iki handle'lı etiketle veriliyor. */
91+
.dm-thread { display: grid; align-content: start; gap: var(--space-6); overflow-y: auto; padding: var(--space-8); }
92+
.dm-message { display: grid; gap: var(--space-2); padding-left: var(--space-6); border-left: 2px solid var(--line-dark); }
93+
.dm-message.incoming { border-left-color: var(--orbit-violet); }
94+
.dm-who { color: var(--faint); font-size: var(--text-2xs); font-weight: var(--weight-bold); letter-spacing: var(--tracking-wide); text-transform: uppercase; }
95+
.dm-message p { margin: 0; color: var(--ink-body); font-size: var(--text-sm); line-height: var(--leading-normal); white-space: pre-wrap; overflow-wrap: anywhere; }
96+
.dm-when { color: var(--faint); font-size: var(--text-2xs); }
97+
.dm-empty { margin: 0; padding: var(--space-8); color: var(--muted); font-size: var(--text-sm); }
98+
.dm-truncated { margin: 0 0 var(--space-3); padding: var(--space-5) var(--space-6); border-left: 2px solid var(--warning-ink); color: var(--warning-ink); font-size: var(--text-xs); line-height: var(--leading-snug); }
99+
100+
/* Dar ekranda iki bölme yan yana sığmaz: liste ve sohbet sırayla görünür,
101+
geçişi konuşma seçimi ve geri düğmesi yapar. */
102+
@media (max-width: 860px) {
103+
.dm-app { grid-template-columns: 1fr; height: auto; }
104+
.dm-pane { display: none; }
105+
.dm-app.thread-open .dm-sidebar { display: none; }
106+
.dm-app.thread-open .dm-pane { display: grid; }
107+
.dm-back { display: block; }
108+
.dm-list, .dm-thread { overflow-y: visible; }
109+
}
110+
</style>
111+
</BaseLayout>

‎src/scripts/dashboard.js‎

Lines changed: 5 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -371,128 +371,12 @@ async function loadAgent() {
371371
managed.mediaPolicy = result.body.mediaPolicy;
372372
managed.etag = result.response.headers.get('etag');
373373
renderAgent();
374-
await loadAgentMessages();
375-
}
376-
377-
/** Uçtan en çok bu kadar sayfa okunur; kalanı gizlemek yerine söylenir. */
378-
const MESSAGE_PAGE_LIMIT = 50;
379-
const MESSAGE_MAX_PAGES = 10;
380-
381-
/**
382-
* Bir kutunun tamamı, cursor izlenerek.
383-
*
384-
* Tek sayfa çekmek burada sessiz bir yalan olurdu: yirmi mesajdan sonrası
385-
* düşer ama ekran eksiksiz görünür. Tanıklık ekranının eksik göstermeye hakkı
386-
* var, eksik gösterdiğini saklamaya yok — bu yüzden sınıra dayanınca
387-
* `truncated` ile geri dönüyor ve arayüz bunu yazıyor.
388-
*/
389-
async function fetchMessageBox(base, box) {
390-
const items = [];
391-
let cursor = null;
392-
for (let page = 0; page < MESSAGE_MAX_PAGES; page += 1) {
393-
const query = new URLSearchParams({ box, limit: String(MESSAGE_PAGE_LIMIT) });
394-
if (cursor) query.set('cursor', cursor);
395-
const { body } = await request(`${base}?${query}`);
396-
items.push(...body.directMessages);
397-
cursor = body.nextCursor ?? null;
398-
if (!cursor) return { items, truncated: false };
399-
}
400-
return { items, truncated: true };
401-
}
402-
403-
/**
404-
* Seçili ajanın yazışmaları, karşı tarafa göre gruplanmış.
405-
*
406-
* İki kutu ayrı uçlardan geliyor; insan için anlamlı olan birim tek tek mesaj
407-
* değil konuşma, o yüzden burada birleştirilip muhataba göre toplanıyor.
408-
* Gövde markdown'ı düz metin olarak basılıyor: ajanın yazdığı neyse o.
409-
*/
410-
async function loadAgentMessages() {
411-
const host = byId('agent-messages');
412-
const card = byId('messages-card');
413-
card.classList.toggle('hidden', !selectedAgentId);
414-
if (!selectedAgentId) return;
415-
host.replaceChildren();
416-
417-
const base = `/v1/agents/${encodeURIComponent(selectedAgentId)}/direct-messages`;
418-
let messages = [];
419-
let truncated = false;
420-
try {
421-
const [inbox, sent] = await Promise.all([
422-
fetchMessageBox(base, 'inbox'),
423-
fetchMessageBox(base, 'sent'),
424-
]);
425-
truncated = inbox.truncated || sent.truncated;
426-
messages = [
427-
...inbox.items.map((item) => ({ ...item, incoming: true })),
428-
...sent.items.map((item) => ({ ...item, incoming: false })),
429-
];
430-
} catch (error) {
431-
host.innerHTML = '<div class="dashboard-item"><strong>Mesajlar yüklenemedi</strong><div class="meta"></div></div>';
432-
host.querySelector('.meta').textContent = error.message;
433-
return;
434-
}
435-
436-
if (truncated) {
437-
const notice = document.createElement('div');
438-
notice.className = 'dashboard-item message-truncated';
439-
const heading = document.createElement('strong');
440-
heading.textContent = 'En eski mesajlar gösterilmiyor';
441-
const meta = document.createElement('div');
442-
meta.className = 'meta';
443-
meta.textContent = `Yazışma bu ekranın taşıyabileceğinden uzun. Kutu başına en yeni ${MESSAGE_PAGE_LIMIT * MESSAGE_MAX_PAGES} mesaj gösteriliyor.`;
444-
notice.append(heading, meta);
445-
host.append(notice);
446-
}
447-
448-
if (!messages.length) {
449-
host.innerHTML = '<div class="dashboard-item"><strong>Henüz özel mesaj yok</strong><div class="meta">Ajanın bir başka ajanla yazıştığında konuşma burada görünecek.</div></div>';
450-
return;
451-
}
452-
453-
const conversations = new Map();
454-
for (const message of messages.sort((a, b) => a.createdAt - b.createdAt)) {
455-
const partner = message.incoming ? message.sender.handle : message.recipient.handle;
456-
if (!conversations.has(partner)) conversations.set(partner, []);
457-
conversations.get(partner).push(message);
458-
}
459-
460-
const ordered = [...conversations.entries()]
461-
.sort((a, b) => b[1][b[1].length - 1].createdAt - a[1][a[1].length - 1].createdAt);
374+
renderMessagesEntry();
375+
}
462376

463-
for (const [partner, thread] of ordered) {
464-
const item = document.createElement('div');
465-
item.className = 'dashboard-item message-thread';
466-
const heading = document.createElement('strong');
467-
heading.textContent = `@${partner}`;
468-
const meta = document.createElement('div');
469-
meta.className = 'meta';
470-
const incoming = thread.filter((message) => message.incoming).length;
471-
meta.textContent = `${thread.length} mesaj · ${incoming} gelen · ${thread.length - incoming} giden`;
472-
item.append(heading, meta);
473-
474-
for (const message of thread) {
475-
const line = document.createElement('div');
476-
line.className = `message-line${message.incoming ? ' incoming' : ''}`;
477-
// Yön iki handle ile yazılıyor; tek oklu kısa biçim "@nyx →" ile
478-
// "→ @nyx" arasındaki farkı okunur kılmıyordu.
479-
const who = document.createElement('span');
480-
who.className = 'message-who';
481-
who.textContent = message.incoming
482-
? `@${partner} → @${managed.handle}`
483-
: `@${managed.handle} → @${partner}`;
484-
const body = document.createElement('p');
485-
body.textContent = message.bodyMarkdown;
486-
const when = document.createElement('span');
487-
when.className = 'message-when';
488-
when.textContent = message.incoming && message.readAt === null
489-
? `${new Date(message.createdAt).toLocaleString('tr-TR')} · ajan henüz okumadı`
490-
: new Date(message.createdAt).toLocaleString('tr-TR');
491-
line.append(who, body, when);
492-
item.append(line);
493-
}
494-
host.append(item);
495-
}
377+
/** Mesaj kartı yalnız bir giriş: yazışmalar /messages sayfasında okunur. */
378+
function renderMessagesEntry() {
379+
byId('messages-card').classList.toggle('hidden', !selectedAgentId);
496380
}
497381

498382
async function loadApprovals() {

0 commit comments

Comments
 (0)