Skip to content
Merged
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
119 changes: 107 additions & 12 deletions src/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2202,13 +2202,10 @@ body {

.history-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.55rem 0.8rem;
flex-direction: column;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
cursor: pointer;
transition: all var(--transition-smooth);
text-align: left;
}
Expand All @@ -2219,20 +2216,37 @@ body {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.history-item.expanded {
border-color: rgba(59, 130, 246, 0.35);
background: rgba(255, 255, 255, 0.05);
}

.history-item-main {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 0.8rem;
cursor: pointer;
border-radius: 8px;
}

.history-item-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
gap: 0.2rem;
}

.history-item-title {
font-size: 0.8rem;
color: var(--text);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-word;
}

.history-item-subtitle {
Expand All @@ -2243,18 +2257,99 @@ body {
white-space: nowrap;
}

.history-item-subtitle.is-case {
color: #7aa7ff;
font-weight: 500;
}

.history-item-meta {
font-size: 0.7rem;
color: var(--text-muted);
white-space: nowrap;
align-self: center;
}

.history-item-size {
font-size: 0.7rem;
.history-item-expand {
appearance: none;
border: none;
background: transparent;
color: var(--text-dim);
white-space: nowrap;
min-width: 45px;
text-align: right;
font-size: 1.1rem;
line-height: 1;
padding: 0.25rem 0.4rem;
cursor: pointer;
border-radius: 6px;
transition: background var(--transition-smooth), color var(--transition-smooth);
flex-shrink: 0;
}

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

.history-item.expanded .history-item-expand {
color: #7aa7ff;
transform: rotate(90deg);
}

.history-item-detail {
padding: 0.5rem 0.8rem 0.7rem;
border-top: 1px dashed rgba(255, 255, 255, 0.08);
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.72rem;
color: var(--text-muted);
}

.history-item-detail[hidden] {
display: none;
}

.history-detail-row {
display: flex;
gap: 0.5rem;
align-items: flex-start;
word-break: break-word;
}

.history-detail-label {
color: var(--text-dim);
min-width: 46px;
flex-shrink: 0;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding-top: 0.1rem;
}

.history-detail-value {
color: var(--text);
flex: 1;
min-width: 0;
white-space: pre-wrap;
}

.history-detail-path {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 0.68rem;
color: #a8b5c9;
}

.history-detail-meta {
color: var(--text-dim);
font-size: 0.68rem;
}

@media (max-width: 640px) {
.history-item-meta {
font-size: 0.65rem;
}
.history-item-main {
gap: 0.45rem;
padding: 0.6rem 0.7rem;
}
}

.history-show-more {
Expand Down
111 changes: 100 additions & 11 deletions src/web/public/terminal-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -845,8 +845,40 @@ Object.assign(CodemanApp.prototype, {
return items;
},

/**
* Resolve workingDir to a case-aware short label.
* - Exact case path match → "#caseName"
* - workingDir under a case dir → "#caseName/subdir"
* - Otherwise → basename (e.g. "Claudeman")
*/
_resolveCaseLabel(workingDir, cases) {
if (!workingDir) return '';
let best = null;
for (const c of cases || []) {
if (!c || !c.path) continue;
if (workingDir === c.path) {
return `#${c.name}`;
}
if (workingDir.startsWith(c.path + '/')) {
const len = c.path.length;
if (!best || len > best.len) {
best = { name: c.name, suffix: workingDir.slice(len), len };
}
}
}
if (best) return `#${best.name}${best.suffix}`;
return workingDir.split('/').pop() || workingDir;
},

/** Normalize home prefixes to "~/" on both Linux and macOS */
_shortenHomePath(p) {
return (p || '')
.replace(/^\/home\/[^/]+\//, '~/')
.replace(/^\/Users\/[^/]+\//, '~/');
},

/** Build a single history item DOM element */
_buildHistoryItem(s) {
_buildHistoryItem(s, cases) {
const size =
s.sizeBytes < 1024
? `${s.sizeBytes}B`
Expand All @@ -858,12 +890,17 @@ Object.assign(CodemanApp.prototype, {
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 shortDir = this._shortenHomePath(s.workingDir);
const caseLabel = this._resolveCaseLabel(s.workingDir, cases);

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

// Main row: clickable surface that triggers resume
const mainRow = document.createElement('div');
mainRow.className = 'history-item-main';
mainRow.addEventListener('click', () => this.resumeHistorySession(s.sessionId, s.workingDir));

const textCol = document.createElement('div');
textCol.className = 'history-item-text';
Expand All @@ -874,19 +911,63 @@ Object.assign(CodemanApp.prototype, {

const subtitleSpan = document.createElement('span');
subtitleSpan.className = 'history-item-subtitle';
subtitleSpan.textContent = shortDir;
if (caseLabel.startsWith('#')) subtitleSpan.classList.add('is-case');
subtitleSpan.textContent = caseLabel;

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;
const expandBtn = document.createElement('button');
expandBtn.className = 'history-item-expand';
expandBtn.type = 'button';
expandBtn.setAttribute('aria-label', 'Show details');
expandBtn.setAttribute('aria-expanded', 'false');
expandBtn.textContent = '⋯'; // ⋯

mainRow.append(textCol, metaSpan, expandBtn);

// Detail panel: full prompt + full path, hidden by default
const detail = document.createElement('div');
detail.className = 'history-item-detail';
detail.hidden = true;

const promptRow = document.createElement('div');
promptRow.className = 'history-detail-row';
const promptLabel = document.createElement('span');
promptLabel.className = 'history-detail-label';
promptLabel.textContent = 'Prompt';
const promptText = document.createElement('span');
promptText.className = 'history-detail-value history-detail-prompt';
promptText.textContent = s.firstPrompt || '(no prompt captured)';
promptRow.append(promptLabel, promptText);

const pathRow = document.createElement('div');
pathRow.className = 'history-detail-row';
const pathLabel = document.createElement('span');
pathLabel.className = 'history-detail-label';
pathLabel.textContent = 'Path';
const pathText = document.createElement('span');
pathText.className = 'history-detail-value history-detail-path';
pathText.textContent = shortDir;
pathRow.append(pathLabel, pathText);

const metaRow = document.createElement('div');
metaRow.className = 'history-detail-row history-detail-meta';
metaRow.textContent = `${timeStr} · ${size} · ${s.sessionId.slice(0, 8)}`;

detail.append(promptRow, pathRow, metaRow);

expandBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
const expanded = item.classList.toggle('expanded');
detail.hidden = !expanded;
expandBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
});

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

Expand All @@ -899,7 +980,15 @@ Object.assign(CodemanApp.prototype, {
if (!container || !list) return;

try {
const allSessions = await this._fetchHistorySessions(30);
// Load cases in parallel so subtitle can show "#caseName" labels.
// Prefer already-loaded this.cases to avoid an extra request.
const casesPromise = Array.isArray(this.cases) && this.cases.length > 0
? Promise.resolve(this.cases)
: fetch('/api/cases').then((r) => (r.ok ? r.json() : [])).catch(() => []);
const [allSessions, cases] = await Promise.all([
this._fetchHistorySessions(30),
casesPromise,
]);
if (allSessions.length === 0) {
container.style.display = 'none';
return;
Expand All @@ -910,7 +999,7 @@ Object.assign(CodemanApp.prototype, {

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

// Add "Show More" button if there are more items
Expand All @@ -920,7 +1009,7 @@ Object.assign(CodemanApp.prototype, {
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);
list.insertBefore(this._buildHistoryItem(allSessions[i], cases), moreBtn);
}
moreBtn.remove();
});
Expand Down