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
29 changes: 23 additions & 6 deletions apps/desktop/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import {
deriveDashboardSkill,
normalizeEditableTags
} from './dashboardMetadata.js';
import { normalizeHistory } from './historyEntries.js';
import {
historyRequestForFilter,
isHistoryRequestCurrent,
normalizeHistory
} from './historyEntries.js';
import {
normalizeDoctorReport,
normalizeStaleDeploymentRepairResult
Expand Down Expand Up @@ -451,6 +455,7 @@ export default function App() {
const appUpdateAutoCheckedRef = useRef(false);
const usageRankingRequestRef = useRef(0);
const rankingImportRequestRef = useRef(0);
const historyRequestRef = useRef(0);
const authoritativeGenerationRef = useRef(0);
const pageRef = useRef(page);
const dismissNotice = () => setNotice('');
Expand Down Expand Up @@ -1941,6 +1946,9 @@ export default function App() {
if (nextPage !== 'rankings') {
cancelUsageRankingRequest();
}
if (nextPage !== 'history') {
historyRequestRef.current += 1;
}
setPage(nextPage);
}

Expand All @@ -1960,24 +1968,33 @@ export default function App() {
function openHistory() {
setSelectedName('');
navigateToPage('history');
void loadHistory();
void loadHistory(historyFilter);
}

async function loadHistory() {
async function loadHistory(nextFilter = historyFilter) {
const requestId = historyRequestRef.current + 1;
historyRequestRef.current = requestId;
setHistoryFilter(nextFilter);
setError('');
setHistory((current) => ({ ...current, entries: [] }));

if (!window.__TAURI_INTERNALS__) {
setHistory(normalizeHistory(previewHistory()));
if (!isHistoryRequestCurrent(historyRequestRef.current, requestId)) return;
setHistory(normalizeHistory(previewHistory(nextFilter)));
setStatus('prototype');
return;
}

setStatus('loading_history');
try {
const historyResult = await invoke('list_history', { request: { limit: 200 } });
const historyResult = await invoke('list_history', {
request: historyRequestForFilter(nextFilter)
});
if (!isHistoryRequestCurrent(historyRequestRef.current, requestId)) return;
setHistory(normalizeHistory(historyResult));
setStatus('ready');
} catch (historyError) {
if (!isHistoryRequestCurrent(historyRequestRef.current, requestId)) return;
setError(historyError.message || String(historyError) || 'Unable to load history.');
setStatus('ready');
}
Expand Down Expand Up @@ -4069,7 +4086,7 @@ export default function App() {
filter={historyFilter}
history={history}
status={status}
onFilter={setHistoryFilter}
onFilter={loadHistory}
onRefresh={loadHistory}
/>
) : page === 'rankings' ? (
Expand Down
29 changes: 25 additions & 4 deletions apps/desktop/src/cardLayout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,16 @@ test('history page combines skill usage and operation logs', () => {
assert.match(historyPageSource, /<PageTitleRow[\s\S]*actions=\{\([\s\S]*onClick=\{onRefresh\}/);
assert.match(historyPageSource, /className="dashboardFilterBar pageTypeFilterBar" aria-label="History filters"[\s\S]*className="dashboardTypeTabs historyTypeTabs"/);
assert.doesNotMatch(historyPageSource, /dashboardControlRow historyControlRow/);
assert.match(appSource, /invoke\('list_history',\s*\{ request: \{ limit: 200 \} \}\)/);
assert.match(appSource, /historyRequestForFilter\(nextFilter\)/);
assert.match(appSource, /request: historyRequestForFilter\(nextFilter\)/);
assert.match(appSource, /const historyRequestRef = useRef\(0\);/);
assert.match(
appSource,
/const requestId = historyRequestRef\.current \+ 1;[\s\S]*historyRequestRef\.current = requestId;/
);
assert.match(appSource, /isHistoryRequestCurrent\(historyRequestRef\.current, requestId\)/);
assert.match(appSource, /onFilter=\{loadHistory\}/);
assert.match(appSource, /onRefresh=\{loadHistory\}/);
assert.match(appSource, /page === 'history'/);
assert.match(appSource, /function normalizeHistory/);
assert.match(appSource, /skillUsageCount/);
Expand All @@ -920,7 +929,10 @@ test('history page combines skill usage and operation logs', () => {
assert.match(appSource, /const rowSubtitle = historyRowSubtitle\(entry, isUsage \|\| isReference\);/);
assert.match(appSource, /function historyRowSubtitle\(entry, isUsage\)/);
assert.match(appSource, /const defaultOperationSubtitle = entry\.operationType && entry\.actor/);
assert.match(appSource, /const groupedEntries = groupHistoryEntriesByDay\(filteredEntries\)/);
assert.match(appSource, /const groupedEntries = groupHistoryEntriesByDay\(entries\)/);
assert.doesNotMatch(historyPageSource, /entries\.filter\(\(entry\) => entry\.kind === filter\)/);
assert.match(historyPageSource, /Loading history\.\.\./);
assert.match(historyPageSource, /role="status" aria-live="polite"/);
assert.match(appSource, /function groupHistoryEntriesByDay/);
assert.match(appSource, /className="historyDayBlock"/);
assert.match(appSource, /function HistoryRow/);
Expand Down Expand Up @@ -1140,12 +1152,21 @@ test('rankings is an accessible top-level page separate from history', () => {
assert.match(appSource, /includeUnmanaged: true/);
assert.match(appSource, /Not imported/);
assert.match(appSource, /Includes skills not imported into SkillBox/);
const loadHistorySource = appComponentSource.match(/async function loadHistory\(\)[\s\S]*?function openRankings/)?.[0] || '';
const loadHistorySource = appComponentSource.match(/async function loadHistory\(nextFilter = historyFilter\)[\s\S]*?function openRankings/)?.[0] || '';
assert.match(loadHistorySource, /invoke\('list_history'/);
assert.doesNotMatch(loadHistorySource, /list_skill_usage_rankings|Promise\.all/);
assert.match(tauriSource, /async fn list_skill_usage_rankings/);
assert.match(tauriSource, /skillbox_core::list_skill_usage_rankings/);
assert.match(css, /\.historyTypeTabs\s*\{[^}]*repeat\(3,/s);
assert.match(css, /\.historyTypeTabs\s*\{[^}]*repeat\(4,/s);
assert.match(
css,
/@media \(max-width: 1180px\) \{[\s\S]*?\.historyTypeTabs\s*\{[^}]*grid-template-columns:\s*repeat\(2,/s
);
assert.match(
css,
/@media \(max-width: 1180px\) \{[\s\S]*?\.historyTypeTabs\s*\{[^}]*height:\s*auto;[\s\S]*?min-height:\s*46px;/s
);
assert.doesNotMatch(css, /\.historyTypeTabs\s*\{[^}]*repeat\(3,/s);
assert.match(css, /\.usageRankingTable\s*\{/);
assert.match(css, /\.usageRankingTopGrid\s*\{/);
assert.match(css, /\.usageRankingTopCard\.leader\s*\{/);
Expand Down
13 changes: 8 additions & 5 deletions apps/desktop/src/components/history.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ export function HistoryPage({
},
{ id: 'operation', label: 'Operations', count: numberOrZero(history.operationCount) }
];
const filteredEntries =
filter === 'all' ? entries : entries.filter((entry) => entry.kind === filter);
const groupedEntries = groupHistoryEntriesByDay(filteredEntries);
const isLoading = status === 'loading_history';
const visibleCount = filteredEntries.length;
const groupedEntries = groupHistoryEntriesByDay(entries);
const visibleCount = isLoading ? 0 : entries.length;

return (
<PageFrame ariaLabel="History">
Expand Down Expand Up @@ -76,7 +74,12 @@ export function HistoryPage({
</div>
</div>

{filteredEntries.length > 0 ? (
{isLoading ? (
<div className="emptyState dashboardEmptyState historyEmptyState" role="status" aria-live="polite">
<strong>Loading history...</strong>
<span>Loading the selected history filter.</span>
</div>
) : entries.length > 0 ? (
<div className="historyTimeline" aria-label="History entries">
{groupedEntries.map((group) => (
<section className="historyDayBlock" key={group.key} aria-label={`${group.label} history`}>
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/historyEntries.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { numberOrZero } from './skills.js';

export const HISTORY_PAGE_LIMIT = 200;

const HISTORY_FILTER_KINDS = Object.freeze({
all: null,
skill_usage: 'skill_usage',
usage_reference: 'usage_reference',
operation: 'operation'
});

export function historyFilterKind(filter = 'all') {
return HISTORY_FILTER_KINDS[filter] || null;
}

export function historyRequestForFilter(filter = 'all') {
const request = { limit: HISTORY_PAGE_LIMIT };
const kind = historyFilterKind(filter);
if (kind) request.kind = kind;
return request;
}

export function isHistoryRequestCurrent(currentRequestId, requestId) {
return currentRequestId === requestId;
}

export function filterHistoryEntries(entries = [], filter = 'all') {
const kind = historyFilterKind(filter);
return kind ? entries.filter((entry) => entry.kind === kind) : entries;
}

export function normalizeHistory(result = {}) {
const entries = (result?.entries || []).map((entry) => ({
id: entry.id || '',
Expand Down
42 changes: 41 additions & 1 deletion apps/desktop/src/historyEntries.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { groupHistoryEntriesByDay } from './historyEntries.js';
import {
filterHistoryEntries,
groupHistoryEntriesByDay,
isHistoryRequestCurrent,
historyRequestForFilter
} from './historyEntries.js';

test('maps History filters to bounded server-side queries', () => {
assert.deepEqual(historyRequestForFilter('all'), { limit: 200 });
assert.deepEqual(historyRequestForFilter('skill_usage'), {
limit: 200,
kind: 'skill_usage'
});
assert.deepEqual(historyRequestForFilter('usage_reference'), {
limit: 200,
kind: 'usage_reference'
});
assert.deepEqual(historyRequestForFilter('operation'), {
limit: 200,
kind: 'operation'
});
});

test('preview history filtering uses the same selected-kind contract', () => {
const entries = [
{ id: 'call', kind: 'skill_usage' },
{ id: 'reference', kind: 'usage_reference' },
{ id: 'operation', kind: 'operation' }
];

assert.deepEqual(
filterHistoryEntries(entries, 'usage_reference').map((entry) => entry.id),
['reference']
);
assert.deepEqual(filterHistoryEntries(entries, 'all'), entries);
});

test('stale History responses and errors cannot pass the request generation gate', () => {
assert.equal(isHistoryRequestCurrent(2, 1), false);
assert.equal(isHistoryRequestCurrent(2, 2), true);
});

test('groups entries from the same calendar day together', () => {
const groups = groupHistoryEntriesByDay([
Expand Down
93 changes: 48 additions & 45 deletions apps/desktop/src/previewData.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { compactPath, defaultSkillStatus, joinPath } from './skills.js';
import { filterHistoryEntries } from './historyEntries.js';

export const previewPaths = {
root: '~/.skillbox',
Expand Down Expand Up @@ -472,56 +473,58 @@ export function previewUserSkillsInbound(mode = 'behind') {
};
}

export function previewHistory() {
export function previewHistory(filter = 'all') {
const now = Date.now();
const entries = [
{
id: 'preview-usage-release-helper',
kind: 'skill_usage',
timestamp: new Date(now - 12 * 60 * 1000).toISOString(),
title: 'Skill call: release-helper',
subtitle: 'codex in ~/.agents/skills',
skill_name: 'release-helper',
agent_id: 'codex',
runtime_root: '~/.agents/skills'
},
{
id: 'preview-operation-install',
kind: 'operation',
timestamp: Math.floor((now - 42 * 60 * 1000) / 1000).toString(),
title: 'Installed docs-reviewer',
subtitle: 'install_remote_skill by desktop',
status: 'succeeded',
operation_type: 'install_remote_skill',
actor: 'desktop',
entity_type: 'skill',
entity_name: 'docs-reviewer'
},
{
id: 'preview-reference-research-digest',
kind: 'usage_reference',
timestamp: new Date(now - 70 * 60 * 1000).toISOString(),
title: 'History reference: research-digest',
subtitle: 'cursor in ~/.cursor/skills',
skill_name: 'research-digest',
agent_id: 'cursor',
runtime_root: '~/.cursor/skills'
},
{
id: 'preview-usage-design-audit',
kind: 'skill_usage',
timestamp: new Date(now - 2 * 60 * 60 * 1000).toISOString(),
title: 'Skill call: design-audit',
subtitle: 'claude-code in ~/.claude/skills',
skill_name: 'design-audit',
agent_id: 'claude-code',
runtime_root: '~/.claude/skills'
}
];

return {
skill_usage_count: 3,
skill_reference_count: 1,
operation_count: 2,
entries: [
{
id: 'preview-usage-release-helper',
kind: 'skill_usage',
timestamp: new Date(now - 12 * 60 * 1000).toISOString(),
title: 'Skill call: release-helper',
subtitle: 'codex in ~/.agents/skills',
skill_name: 'release-helper',
agent_id: 'codex',
runtime_root: '~/.agents/skills'
},
{
id: 'preview-operation-install',
kind: 'operation',
timestamp: Math.floor((now - 42 * 60 * 1000) / 1000).toString(),
title: 'Installed docs-reviewer',
subtitle: 'install_remote_skill by desktop',
status: 'succeeded',
operation_type: 'install_remote_skill',
actor: 'desktop',
entity_type: 'skill',
entity_name: 'docs-reviewer'
},
{
id: 'preview-reference-research-digest',
kind: 'usage_reference',
timestamp: new Date(now - 70 * 60 * 1000).toISOString(),
title: 'History reference: research-digest',
subtitle: 'cursor in ~/.cursor/skills',
skill_name: 'research-digest',
agent_id: 'cursor',
runtime_root: '~/.cursor/skills'
},
{
id: 'preview-usage-design-audit',
kind: 'skill_usage',
timestamp: new Date(now - 2 * 60 * 60 * 1000).toISOString(),
title: 'Skill call: design-audit',
subtitle: 'claude-code in ~/.claude/skills',
skill_name: 'design-audit',
agent_id: 'claude-code',
runtime_root: '~/.claude/skills'
}
]
entries: filterHistoryEntries(entries, filter)
};
}

Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1638,7 +1638,7 @@ button {
.historyTypeTabs {
width: 580px;
flex: 0 0 auto;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
}

.usageRankingPanel {
Expand Down Expand Up @@ -6722,6 +6722,13 @@ dd {
grid-template-columns: repeat(4, minmax(0, 1fr));
}

.historyTypeTabs {
width: min(580px, 100%);
height: auto;
min-height: 46px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}

.sideStack {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
Expand Down Expand Up @@ -6865,7 +6872,7 @@ dd {

.historyTypeTabs {
width: 100%;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
}

.usageRankingControls {
Expand Down
Loading