Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 357 additions & 5 deletions internal/render/templates/day.html

Large diffs are not rendered by default.

67 changes: 60 additions & 7 deletions internal/render/templates/dir.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,61 @@
<body class="{{ .ThemeClass }}">
<header>
<p class="subtitle"><a href="/?view=dir">All directories</a></p>
<h1 class="page-title">Dates for {{ .Dir.Label }}</h1>
<h1 class="page-title">Sessions for {{ .Dir.Label }}</h1>
<p class="meta">{{ .Dir.Count }} session{{ if ne .Dir.Count 1 }}s{{ end }}</p>
<form class="filter-form" method="get" action="/dir">
<input type="hidden" name="cwd" value="{{ .Dir.Value }}">
<input type="hidden" name="page" value="1">
<label class="filter-toggle">
<input class="filter-checkbox" type="checkbox" name="all" value="1"{{ if .ShowAll }} checked{{ end }} onchange="this.form.submit()">
<span class="filter-label">Show all (including threads without user messages)</span>
</label>
</form>
</header>
<main>
{{ if or .HasPrev .HasNext (gt .TotalPages 1) }}
<div class="pagination">
{{ if .HasPrev }}
<a class="nav-btn" href="/dir?cwd={{ $.Dir.Value | urlquery }}&page={{ .PrevPage }}{{ if $.ShowAll }}&all=1{{ end }}">⬅️ Prev</a>
{{ else }}
<span class="nav-btn disabled">⬅️ Prev</span>
{{ end }}
<span class="page-meta">Page {{ .Page }}{{ if gt .TotalPages 0 }} / {{ .TotalPages }}{{ end }}</span>
{{ if .HasNext }}
<a class="nav-btn" href="/dir?cwd={{ $.Dir.Value | urlquery }}&page={{ .NextPage }}{{ if $.ShowAll }}&all=1{{ end }}">Next ➡️</a>
{{ else }}
<span class="nav-btn disabled">Next ➡️</span>
{{ end }}
</div>
{{ end }}
<div class="card">
{{ if .Dates }}
{{ if .Sessions }}
<ul class="list link-list">
{{ range .Dates }}
<li>
<a class="link-item-link" href="/{{ .Path }}/?cwd={{ $.Dir.Value | urlquery }}">
{{ .Label }}
<span class="meta">{{ .Count }} session{{ if ne .Count 1 }}s{{ end }}</span>
{{ range .Sessions }}
<li class="session-list-item">
<a class="link-item-link" href="/{{ .DatePath }}/{{ .Name }}#last-item">
{{ .Name }}
<span class="meta">{{ .ModTime }} | {{ .Size }}</span>
</a>
{{ if or .LastUserSnippet .LastAssistantSnippet }}
<div class="session-snippet session-snippet-thread">
{{ if .LastUserSnippet }}<div class="snippet-divider">:</div>{{ end }}
{{ if .LastUserSnippet }}
<a class="snippet-link" href="/{{ .DatePath }}/{{ .Name }}#last-item">
<div class="session-item role-user snippet-item">
<div class="session-content">{{ .LastUserSnippet }}</div>
</div>
</a>
{{ end }}
{{ if .LastAssistantSnippet }}
<a class="snippet-link" href="/{{ .DatePath }}/{{ .Name }}#last-item">
<div class="session-item role-assistant snippet-item">
<div class="session-content">{{ .LastAssistantSnippet }}</div>
</div>
</a>
{{ end }}
</div>
{{ end }}
</li>
{{ end }}
</ul>
Expand All @@ -31,6 +73,17 @@ <h1 class="page-title">Dates for {{ .Dir.Label }}</h1>
{{ end }}
</div>
</main>
<script>
(function () {
document.addEventListener("click", function (event) {
var item = event.target.closest(".session-list-item");
if (!item || event.target.closest("a")) return;
var link = item.querySelector("a.link-item-link");
if (!link) return;
window.location = link.href;
});
})();
</script>
</body>
</html>
{{ end }}
66 changes: 58 additions & 8 deletions internal/render/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,15 @@ <h1 class="page-title">{{ if eq .View "dir" }}Available Directories{{ else }}Ava
setStatus(data.results.length + " result" + (data.results.length === 1 ? "" : "s") + ".");
data.results.forEach(function (item) {
var li = document.createElement("li");
li.className = "search-result";
li.className = "session-list-item search-result";

var link = document.createElement("a");
link.className = "search-result-link";
link.href = "/" + item.path + "/" + item.file + "#line-" + item.line;
link.className = "link-item-link search-result-link";
var targetLine = item.line;
if (item.role === "assistant" && item.prevUserLine) {
targetLine = item.prevUserLine;
}
link.href = "/" + item.path + "/" + item.file + "#line-" + targetLine;
link.textContent = item.file;

var meta = document.createElement("span");
Expand All @@ -163,11 +167,57 @@ <h1 class="page-title">{{ if eq .View "dir" }}Available Directories{{ else }}Ava
li.appendChild(link);
li.appendChild(meta);

if (item.preview) {
var snippet = document.createElement("div");
snippet.className = "search-result-snippet";
highlightText(snippet, item.preview, query);
li.appendChild(snippet);
var thread = document.createElement("div");
thread.className = "session-snippet session-snippet-thread";
var hasUserSnippet = false;

function addSnippet(role, text, highlight, line) {
if (!text) return;
var snippetLink = document.createElement("a");
snippetLink.className = "snippet-link";
var resolvedLine = line && line > 0 ? line : targetLine;
snippetLink.href = "/" + item.path + "/" + item.file + "#line-" + resolvedLine;

var itemBox = document.createElement("div");
itemBox.className = "session-item role-" + role + " snippet-item";

var content = document.createElement("div");
content.className = "session-content";
if (highlight) {
highlightText(content, text, query);
} else {
content.textContent = text;
}

itemBox.appendChild(content);
snippetLink.appendChild(itemBox);
thread.appendChild(snippetLink);
}

if (item.role === "user") {
hasUserSnippet = !!item.preview;
addSnippet("user", item.preview, true, item.line);
if (item.nextAssistant) {
addSnippet("assistant", item.nextAssistant, false, item.nextAssistantLine);
}
} else if (item.role === "assistant") {
if (item.prevUser) {
hasUserSnippet = true;
addSnippet("user", item.prevUser, false, item.prevUserLine);
}
addSnippet("assistant", item.preview, true, item.line);
} else if (item.preview) {
addSnippet(item.role || "unknown", item.preview, true, item.line);
}

if (thread.childNodes.length > 0) {
if (hasUserSnippet) {
var divider = document.createElement("div");
divider.className = "snippet-divider";
divider.textContent = ":";
thread.insertBefore(divider, thread.firstChild);
}
li.appendChild(thread);
}

results.appendChild(li);
Expand Down
86 changes: 54 additions & 32 deletions internal/render/templates/session.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
</head>
<body class="{{ .ThemeClass }} has-sticky-header">
<header class="sticky-header">
<p class="subtitle"><a href="/">All dates</a> / <a href="/{{ .Date.Path }}/">{{ .Date.Label }}</a></p>
<h1 class="page-title">{{ .File.Name }}</h1>
<p class="meta">{{ .File.Size }} | {{ .File.ModTime }}{{ if .File.Cwd }} | CWD: {{ .File.Cwd }}{{ if .ResumeCommand }} (<a href="#" data-copy-id="resume-cmd">Copy resume command</a>){{ end }}{{ end }}{{ if and (not .File.Cwd) .ResumeCommand }}
| <a href="#" data-copy-id="resume-cmd">Copy resume command</a>{{ end }} | <a href="#" data-copy-id="md-all">Copy thread as Markdown</a>
{{ if and .IsJSONL (gt .LastUserLine 0) }}| <a href="#line-{{ .LastUserLine }}">Jump to last user message</a> | <a id="jump-user-prev" href="#" aria-label="Previous user message" title="Previous user message">[&uarr;]</a> <a id="jump-user-next" href="#" aria-label="Next user message" title="Next user message">[&darr;]</a>{{ end }}
| <form class="share-form" method="post" action="/share/{{ .Date.Path }}/{{ .File.Name }}">
<button class="copy-btn" type="submit">Share</button>
<p class="subtitle"><a href="/">All dates</a> / {{ .Date.Label }}{{ if .File.ModTimeOnly }} {{ .File.ModTimeOnly }}{{ end }} ({{ .File.Size }})</p>
<h1 class="page-title">{{ .File.Name }}{{ if .ResumeCommand }} <span class="title-links">(<a class="title-link nav-btn" href="#" data-copy-id="resume-cmd">▶️ Copy resume command</a> | <a class="title-link nav-btn" href="#" data-copy-id="md-all">📋 Copy thread as Markdown</a>)</span>{{ end }}</h1>
<p class="meta">{{ if .File.Cwd }}<a class="cwd-link nav-btn" href="/dir?cwd={{ .File.Cwd | urlquery }}" title="Back to this CWD">📂</a> CWD: {{ .File.Cwd }}{{ if .CwdNav }} {{ if .CwdNav.Prev }}<a class="nav-btn" href="{{ .CwdNav.Prev.Path }}" title="{{ .CwdNav.Prev.Title }}">⬅️</a>{{ else }}<span class="nav-btn disabled">⬅️</span>{{ end }}{{ if .CwdNav.Next }}<a class="nav-btn" href="{{ .CwdNav.Next.Path }}" title="{{ .CwdNav.Next.Title }}">➡️</a>{{ else }}<span class="nav-btn disabled">➡️</span>{{ end }}{{ end }}{{ end }}
{{ if and .IsJSONL (gt .LastUserLine 0) }}{{ if .File.Cwd }} | {{ end }}<a class="nav-btn" href="#last-user">🚀 Jump to last user message</a> | <a class="nav-btn" id="jump-user-prev" href="#" aria-label="Previous user message" title="Previous user message">⬆️</a><a class="nav-btn" id="jump-user-next" href="#" aria-label="Next user message" title="Next user message">⬇️</a>{{ end }}
<form class="share-form" method="post" action="/share/{{ .Date.Path }}/{{ .File.Name }}">
<button class="nav-btn" type="submit">Share</button>
</form>
</p>
<div id="share-banner" class="share-banner" role="status" aria-live="polite"></div>
Expand All @@ -39,16 +38,21 @@ <h1 class="page-title">{{ .File.Name }}</h1>

{{ if .Items }}
{{ range .Items }}
<section id="line-{{ .Line }}" class="session-item {{ .Class }}">
<section id="line-{{ .Line }}" class="{{ if .IsTurnAborted }}turn-aborted-item{{ else }}session-item {{ .Class }}{{ end }}">
{{ if eq .Line $.LastUserLine }}<span id="last-user" class="session-anchor"></span>{{ end }}
{{ if eq .Line $.LastAgentLine }}<span id="last-agent" class="session-anchor"></span>{{ end }}
{{ if and (gt $.LastUserLine 0) (eq .Line $.LastUserLine) }}<span id="last-item" class="session-anchor"></span>{{ else if and (eq $.LastUserLine 0) (eq .Line $.LastItemLine) }}<span id="last-item" class="session-anchor"></span>{{ end }}
{{ if .IsTurnAborted }}
<p class="turn-aborted-text">{{ .TurnAbortedMessage }}</p>
{{ else }}
<div class="session-header">
<span class="session-title">{{ .Title }}</span>
<span class="session-type">{{ .Type }}{{ if .Subtype }}:{{ .Subtype }}{{ end }}</span>
<span class="meta">{{ .Timestamp }}</span>
{{ if .Role }}<span class="tag">{{ .Role }}</span>{{ end }}
{{ if .AutoCtx }}<span class="tag tag-auto">Auto context</span>{{ end }}
<span class="meta">Line {{ .Line }}</span>
<button class="copy-btn" type="button" data-copy-id="md-{{ .Line }}" aria-label="Copy Markdown" title="Copy Markdown">📋</button>
<button class="copy-btn" type="button" data-copy-link="line-{{ .Line }}" aria-label="Copy Link" title="Copy Link">🔗</button>
<button class="copy-btn" type="button" data-copy-id="md-{{ .Line }}" aria-label="Copy Markdown" title="Copy Markdown">📋</button><button class="copy-btn" type="button" data-copy-link="line-{{ .Line }}" aria-label="Copy Link" title="Copy Link">🔗</button>
</div>
{{ if eq .Subtype "reasoning" }}
<details>
Expand All @@ -64,6 +68,7 @@ <h1 class="page-title">{{ .File.Name }}</h1>
<div class="session-content markdown">{{ .HTML }}</div>
{{ end }}
<textarea id="md-{{ .Line }}" class="copy-source">{{ .Markdown }}</textarea>
{{ end }}
</section>
{{ end }}
{{ else }}
Expand All @@ -82,6 +87,26 @@ <h1 class="page-title">{{ .File.Name }}</h1>
updateStickyOffset();
window.addEventListener("resize", updateStickyOffset);

function getStickyOffset() {
return stickyHeader ? stickyHeader.offsetHeight : 0;
}
function getElementTop(element) {
var rect = element.getBoundingClientRect();
var scrollY = window.scrollY || window.pageYOffset || 0;
return rect.top + scrollY;
}
function scrollToElement(element) {
if (!element) return;
var offset = getStickyOffset() + 8;
var top = getElementTop(element) - offset;
if (top < 0) top = 0;
if (typeof window.scrollTo === "function") {
window.scrollTo({ top: top, behavior: "smooth" });
} else {
window.scrollTop = top;
}
}

function copyText(text, target) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () {});
Expand Down Expand Up @@ -173,32 +198,13 @@ <h1 class="page-title">{{ .File.Name }}</h1>
var userSections = Array.prototype.slice.call(
document.querySelectorAll("section.session-item.role-user:not(.auto-context)")
);
var getStickyOffset = function () {
return stickyHeader ? stickyHeader.offsetHeight : 0;
};
var getSectionTop = function (section) {
var rect = section.getBoundingClientRect();
var scrollY = window.scrollY || window.pageYOffset || 0;
return rect.top + scrollY;
};
var scrollToSection = function (section) {
if (!section) return;
var offset = getStickyOffset() + 8;
var top = getSectionTop(section) - offset;
if (top < 0) top = 0;
if (typeof window.scrollTo === "function") {
window.scrollTo({ top: top, behavior: "smooth" });
} else {
window.scrollTop = top;
}
};
var findCurrentIndex = function () {
var offset = getStickyOffset() + 8;
var scrollY = window.scrollY || window.pageYOffset || 0;
var position = scrollY + offset + 1;
var current = -1;
for (var i = 0; i < userSections.length; i++) {
if (getSectionTop(userSections[i]) <= position) {
if (getElementTop(userSections[i]) <= position) {
current = i;
} else {
break;
Expand All @@ -211,19 +217,35 @@ <h1 class="page-title">{{ .File.Name }}</h1>
if (!userSections.length) return;
var index = findCurrentIndex();
var target = index <= 0 ? 0 : index - 1;
scrollToSection(userSections[target]);
scrollToElement(userSections[target]);
};
var goNext = function (event) {
if (event) event.preventDefault();
if (!userSections.length) return;
var index = findCurrentIndex();
var target = index < 0 ? 0 : Math.min(index + 1, userSections.length - 1);
scrollToSection(userSections[target]);
scrollToElement(userSections[target]);
};
if (jumpPrev) jumpPrev.addEventListener("click", goPrev);
if (jumpNext) jumpNext.addEventListener("click", goNext);
}

function handleInitialJump() {
var hash = window.location.hash || "";
if (hash.length > 1) {
var target = document.getElementById(hash.slice(1));
if (target) {
setTimeout(function () { scrollToElement(target); }, 0);
return;
}
}
var lastItem = document.getElementById("last-item");
if (lastItem) {
setTimeout(function () { scrollToElement(lastItem); }, 0);
}
}
handleInitialJump();

})();
</script>
</body>
Expand Down
Loading