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
10 changes: 8 additions & 2 deletions src/web/public/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,15 @@ html.mobile-init .file-browser-panel {
Phone Breakpoint (<430px)
============================================================================ */
@media (max-width: 430px) {
/* Hide header brand on phones */
/* Compact header brand on phones — acts as home button */
.header-brand {
display: none;
padding-right: 0.25rem;
margin-right: 0.2rem;
border-right: none;
}

.header-brand .logo {
font-size: 0.7rem;
}

/* Font controls - compact on phones, visibility controlled by JS */
Expand Down
25 changes: 24 additions & 1 deletion src/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,24 @@ body {
text-align: right;
}

.history-show-more {
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
background: rgba(255, 255, 255, 0.04);
border: 1px dashed rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--text-muted);
font-size: 0.75rem;
cursor: pointer;
transition: background var(--transition-smooth), color var(--transition-smooth);
}

.history-show-more:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text);
}

.welcome-hint {
color: var(--text-muted);
font-size: 0.8rem;
Expand Down Expand Up @@ -5485,7 +5503,12 @@ kbd {
/* Responsive */
@media (max-width: 600px) {
.header-brand {
display: none;
padding-right: 0.4rem;
margin-right: 0.3rem;
}

.header-brand .logo {
font-size: 0.75rem;
}

.connection-text {
Expand Down
134 changes: 80 additions & 54 deletions src/web/public/terminal-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -734,84 +734,110 @@ Object.assign(CodemanApp.prototype, {
},

/**
* Fetch and deduplicate history sessions (up to 2 per dir, max `limit` total).
* @returns {Promise<Array>} deduplicated session list, sorted by lastModified desc
* Fetch and deduplicate history sessions (up to 3 per project, sorted by date).
* Uses projectKey for grouping because workingDir decoding is lossy.
* @returns {Promise<Array>} deduplicated session list, most recent first
*/
async _fetchHistorySessions(limit = 12) {
async _fetchHistorySessions() {
const res = await fetch('/api/history/sessions');
const data = await res.json();
const sessions = data.sessions || [];
if (sessions.length === 0) return [];

const byDir = new Map();
const byProject = new Map();
for (const s of sessions) {
if (!byDir.has(s.workingDir)) byDir.set(s.workingDir, []);
byDir.get(s.workingDir).push(s);
const key = s.projectKey || s.workingDir;
if (!byProject.has(key)) byProject.set(key, []);
byProject.get(key).push(s);
}
const items = [];
for (const [, group] of byDir) {
items.push(...group.slice(0, 2));
for (const [, group] of byProject) {
items.push(...group.slice(0, 3));
}
items.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
return items.slice(0, limit);
return items;
},

/** Build a single history item DOM element */
_buildHistoryItem(s) {
const size =
s.sizeBytes < 1024
? `${s.sizeBytes}B`
: s.sizeBytes < 1048576
? `${(s.sizeBytes / 1024).toFixed(0)}K`
: `${(s.sizeBytes / 1048576).toFixed(1)}M`;
const date = new Date(s.lastModified);
const timeStr =
date.toLocaleDateString('en', { month: 'short', day: 'numeric' }) +
' ' +
date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/');

const item = document.createElement('div');
item.className = 'history-item';
item.title = s.workingDir;
item.addEventListener('click', () => this.resumeHistorySession(s.sessionId, s.workingDir));

const textCol = document.createElement('div');
textCol.className = 'history-item-text';

const titleSpan = document.createElement('span');
titleSpan.className = 'history-item-title';
titleSpan.textContent = s.firstPrompt || shortDir;

const subtitleSpan = document.createElement('span');
subtitleSpan.className = 'history-item-subtitle';
subtitleSpan.textContent = shortDir;

textCol.append(titleSpan, subtitleSpan);

const metaSpan = document.createElement('span');
metaSpan.className = 'history-item-meta';
metaSpan.textContent = timeStr;

const sizeSpan = document.createElement('span');
sizeSpan.className = 'history-item-size';
sizeSpan.textContent = size;

item.append(textCol, metaSpan, sizeSpan);
return item;
},

/** Number of history items shown before "Show More" */
_HISTORY_INITIAL_COUNT: 4,

async loadHistorySessions() {
const container = document.getElementById('historySessions');
const list = document.getElementById('historyList');
if (!container || !list) return;

try {
const display = await this._fetchHistorySessions(12);
if (display.length === 0) {
const allSessions = await this._fetchHistorySessions(30);
if (allSessions.length === 0) {
container.style.display = 'none';
return;
}

// Build DOM safely (no innerHTML with user data)
list.replaceChildren();
for (const s of display) {
const size =
s.sizeBytes < 1024
? `${s.sizeBytes}B`
: s.sizeBytes < 1048576
? `${(s.sizeBytes / 1024).toFixed(0)}K`
: `${(s.sizeBytes / 1048576).toFixed(1)}M`;
const date = new Date(s.lastModified);
const timeStr =
date.toLocaleDateString('en', { month: 'short', day: 'numeric' }) +
' ' +
date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/');

const item = document.createElement('div');
item.className = 'history-item';
item.title = s.workingDir;
item.addEventListener('click', () => this.resumeHistorySession(s.sessionId, s.workingDir));

const textCol = document.createElement('div');
textCol.className = 'history-item-text';

const titleSpan = document.createElement('span');
titleSpan.className = 'history-item-title';
titleSpan.textContent = s.firstPrompt || shortDir;

const subtitleSpan = document.createElement('span');
subtitleSpan.className = 'history-item-subtitle';
subtitleSpan.textContent = shortDir;

textCol.append(titleSpan, subtitleSpan);

const metaSpan = document.createElement('span');
metaSpan.className = 'history-item-meta';
metaSpan.textContent = timeStr;

const sizeSpan = document.createElement('span');
sizeSpan.className = 'history-item-size';
sizeSpan.textContent = size;

item.append(textCol, metaSpan, sizeSpan);
list.appendChild(item);
const initialCount = this._HISTORY_INITIAL_COUNT;

// Render initial items
for (let i = 0; i < Math.min(initialCount, allSessions.length); i++) {
list.appendChild(this._buildHistoryItem(allSessions[i]));
}

// Add "Show More" button if there are more items
if (allSessions.length > initialCount) {
const moreBtn = document.createElement('button');
moreBtn.className = 'history-show-more';
moreBtn.textContent = `Show ${allSessions.length - initialCount} more`;
moreBtn.addEventListener('click', () => {
for (let i = initialCount; i < allSessions.length; i++) {
list.insertBefore(this._buildHistoryItem(allSessions[i]), moreBtn);
}
moreBtn.remove();
});
list.appendChild(moreBtn);
}

container.style.display = '';
Expand Down
Loading