Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
464 changes: 417 additions & 47 deletions cli.js

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions cli/session-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ async function listSessionUsageCore(params = {}, deps = {}) {
parseClaudeSessionSummary,
parseCodeBuddySessionSummary,
parseGeminiSessionSummary,
parsePiSessionSummary,
MAX_SESSION_USAGE_LIST_SIZE,
SESSION_BROWSE_SUMMARY_READ_BYTES
} = deps;

const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy' || params.source === 'pi'
? params.source
: 'all';
const rawLimit = Number(params.limit);
Expand Down Expand Up @@ -87,7 +88,9 @@ async function listSessionUsageCore(params = {}, deps = {}) {
? parseGeminiSessionSummary(filePath, summaryOptions)
: (normalized.source === 'codebuddy'
? parseCodeBuddySessionSummary(filePath, summaryOptions)
: parseCodexSessionSummary(filePath, summaryOptions)));
: (normalized.source === 'pi'
? parsePiSessionSummary(filePath, summaryOptions)
: parseCodexSessionSummary(filePath, summaryOptions))));
} catch (_) {
summary = null;
}
Expand Down
19 changes: 19 additions & 0 deletions lib/cli-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ function extractMessageFromRecord(record, source) {
}
return null;
}
if (source === 'pi') {
if (record.type === 'message' && record.message && typeof record.message === 'object') {
const role = normalizeRole(record.message.role);
const text = extractMessageText(record.message.content);
if (!role || !text) {
return null;
}
return { role, text };
}
return null;
}

const role = normalizeRole(record.type);
if (!role) {
Expand Down Expand Up @@ -271,6 +282,14 @@ function applySessionDetailRecordMetadata(record, source, state) {
return;
}

if (source === 'pi') {
if (record.type === 'session') {
state.sessionId = record.id || state.sessionId;
state.cwd = record.cwd || state.cwd;
}
return;
}

if (!state.sessionId && record.sessionId) {
state.sessionId = record.sessionId;
}
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/session-usage-backend.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ function instantiateListSessionUsage(bindings = {}) {
parseGeminiSessionSummary() {
throw new Error('should not parse gemini summary in this test');
},
parsePiSessionSummary() {
throw new Error('should not parse pi summary in this test');
},
...(bindings || {})
};
const bindingNames = Object.keys(effectiveBindings);
Expand Down
10 changes: 5 additions & 5 deletions web-ui/logic.sessions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ function shouldUseFastSessionBrowseLimit(options = {}) {

export function isSessionQueryEnabled(source) {
const normalized = normalizeSessionSource(source, '');
return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'all';
return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all';
}

export function normalizeSessionSource(source, fallback = 'all') {
const normalized = typeof source === 'string'
? source.trim().toLowerCase()
: '';
if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'all') {
if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all') {
return normalized;
}
return fallback;
Expand Down Expand Up @@ -209,7 +209,7 @@ export function buildUsageHeatmap(sessions = [], options = {}) {
for (const session of list) {
if (!session || typeof session !== 'object') continue;
const source = normalizeSessionSource(session.source, '');
if (source !== 'codex' && source !== 'claude') continue;
if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
const updatedAtMs = Date.parse(session.updatedAt || '');
if (!Number.isFinite(updatedAtMs)) continue;
normalized.push({
Expand Down Expand Up @@ -309,7 +309,7 @@ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
for (const session of list) {
if (!session || typeof session !== 'object') continue;
const source = normalizeSessionSource(session.source, '');
if (source !== 'codex' && source !== 'claude') continue;
if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
const updatedAtMs = Date.parse(session.updatedAt || '');
if (!Number.isFinite(updatedAtMs)) continue;
const dayStart = toUtcDayStartMs(updatedAtMs);
Expand Down Expand Up @@ -402,7 +402,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
for (const [sessionIndex, session] of list.entries()) {
if (!session || typeof session !== 'object') continue;
const source = normalizeSessionSource(session.source, '');
if (source !== 'codex' && source !== 'claude') continue;
if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Carry Pi through every chart aggregation field.

Line [405] now admits Pi into buildUsageChartGroups, but the function still treats every non-Codex session as Claude (Lines [491]-[494]), keeps only Codex/Claude counters and source totals (Lines [440]-[453]), and maps source labels the same way (Lines [522]-[524], [594]). For Pi sessions, sourceMessageTotals[source] += messageCount writes to a missing pi property and produces NaN, while totals and labels report Pi as Claude. Add Pi to bucket counters, source totals/share, labels, and total-session calculations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/logic.sessions.mjs` at line 405, Extend buildUsageChartGroups to
handle Pi consistently across all aggregation fields: add Pi bucket counters and
source totals/share entries, route Pi sessions to the Pi bucket instead of the
non-Codex Claude fallback, include Pi in total-session calculations, and map Pi
to its own chart label wherever source labels are generated.

const updatedAtMs = Date.parse(session.updatedAt || '');
if (!Number.isFinite(updatedAtMs)) continue;
const createdAtMs = Date.parse(session.createdAt || '');
Expand Down
3 changes: 2 additions & 1 deletion web-ui/modules/app.computed.session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ export function createSessionComputed() {
{ value: "codex", label: this.t("sessions.source.codex") },
{ value: "claude", label: this.t("sessions.source.claudeCode") },
{ value: "gemini", label: this.t("sessions.source.gemini") },
{ value: "codebuddy", label: this.t("sessions.source.codebuddy") }
{ value: "codebuddy", label: this.t("sessions.source.codebuddy") },
{ value: "pi", label: this.t("sessions.source.pi") }
];
},
activeSessionExportKey() {
Expand Down
16 changes: 12 additions & 4 deletions web-ui/modules/app.methods.session-actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export function createSessionActionMethods(options = {}) {
let error = '';
if (!source) {
error = '缺少 source 参数';
} else if (source !== 'codex' && source !== 'claude') {
error = 'source 仅支持 codex 或 claude';
} else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi') {
error = 'source 仅支持 codex、claude、gemini、codebuddy 或 pi';
Comment on lines +31 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include Pi in the destructive-action capability guard.

Lines [31]-[32] now accept Pi for standalone session URLs, but isDeleteAvailable() (Lines [225]-[231]) still allows only Codex and Claude. Pi sessions can therefore be browsed and represented in trash, but the UI rejects them before calling trash-session or delete-session. Extend that guard to Pi and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 31 - 32, Update
isDeleteAvailable() to treat source 'pi' as supported alongside codex and
claude, allowing Pi sessions to invoke trash-session and delete-session. Add a
regression test covering destructive-action availability for Pi sessions.

}
if (!sessionId && !filePath) {
error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
Expand Down Expand Up @@ -67,7 +67,15 @@ export function createSessionActionMethods(options = {}) {
return;
}

const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
const sourceLabel = context.params.source === 'codex'
? 'Codex'
: (context.params.source === 'claude'
? 'Claude Code'
: (context.params.source === 'gemini'
? 'Gemini CLI'
: (context.params.source === 'pi'
? 'Pi'
: (context.params.source === 'codebuddy' ? 'CodeBuddy Code' : context.params.source))));
this.activeSession = {
source: context.params.source,
sourceLabel,
Expand Down Expand Up @@ -96,7 +104,7 @@ export function createSessionActionMethods(options = {}) {
buildSessionStandaloneUrl(session) {
if (!session) return '';
const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
if (!source || (source !== 'codex' && source !== 'claude')) return '';
if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi')) return '';
const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
if (!sessionId && !filePath) return '';
Expand Down
12 changes: 8 additions & 4 deletions web-ui/modules/app.methods.session-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function createSessionBrowserMethods(options = {}) {
syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
const targetSource = source === 'claude'
? 'claude'
: (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
: (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
? this.sessionPathOptionsMap[targetSource]
: [];
Expand All @@ -137,7 +137,7 @@ export function createSessionBrowserMethods(options = {}) {
refreshSessionPathOptions(source) {
const targetSource = source === 'claude'
? 'claude'
: (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
: (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
? [...this.sessionPathOptionsMap[targetSource]]
: [];
Expand All @@ -161,7 +161,7 @@ export function createSessionBrowserMethods(options = {}) {
async loadSessionPathOptions(options = {}) {
const source = options.source === 'claude'
? 'claude'
: (options.source === 'gemini' ? 'gemini' : (options.source === 'all' ? 'all' : 'codex'));
: (options.source === 'gemini' ? 'gemini' : (options.source === 'pi' ? 'pi' : (options.source === 'all' ? 'all' : 'codex')));
const forceRefresh = !!options.forceRefresh;
const loaded = !!this.sessionPathOptionsLoadedMap[source];
if (!forceRefresh && loaded) {
Expand Down Expand Up @@ -507,7 +507,11 @@ export function createSessionBrowserMethods(options = {}) {
? this.t('sessions.source.claudeCode')
: (this.sessionFilterSource === 'gemini'
? this.t('sessions.source.gemini')
: (this.sessionFilterSource === 'codebuddy' ? this.t('sessions.source.codebuddy') : this.sessionFilterSource)));
: (this.sessionFilterSource === 'codebuddy'
? this.t('sessions.source.codebuddy')
: (this.sessionFilterSource === 'pi'
? this.t('sessions.source.pi')
: this.sessionFilterSource))));
chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
}
if (this.sessionPathFilter) {
Expand Down
4 changes: 2 additions & 2 deletions web-ui/modules/app.methods.session-trash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ export function createSessionTrashMethods(options = {}) {
const deletedAt = typeof result.deletedAt === 'string' && result.deletedAt
? result.deletedAt
: new Date().toISOString();
const source = session && (session.source === 'claude' || session.source === 'gemini')
const source = session && (session.source === 'claude' || session.source === 'gemini' || session.source === 'codebuddy' || session.source === 'pi')
? session.source
: 'codex';
return {
trashId: typeof result.trashId === 'string' ? result.trashId : '',
source,
sourceLabel: session && typeof session.sourceLabel === 'string' && session.sourceLabel
? session.sourceLabel
: (source === 'claude' ? 'Claude Code' : (source === 'gemini' ? 'Gemini CLI' : 'Codex')),
: (source === 'claude' ? 'Claude Code' : (source === 'gemini' ? 'Gemini CLI' : (source === 'pi' ? 'Pi' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')))),
sessionId: session && typeof session.sessionId === 'string' ? session.sessionId : '',
title: session && typeof session.title === 'string' && session.title
? session.title
Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/i18n/locales/en.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ const en = Object.freeze({
'sessions.source.claudeCode': 'Claude Code',
'sessions.source.gemini': 'Gemini CLI',
'sessions.source.codebuddy': 'CodeBuddy Code',
'sessions.source.pi': 'Pi',
'sessions.loadingList': 'Loading sessions...',
'sessions.empty': 'No sessions found',
'sessions.unknownTime': 'unknown time',
Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/i18n/locales/ja.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ const ja = Object.freeze({
'sessions.source.claudeCode': 'Claude Code',
'sessions.source.gemini': 'Gemini',
'sessions.source.codebuddy': 'CodeBuddy',
'sessions.source.pi': 'Pi',
'sessions.loadingList': 'セッション一覧を読み込み中...',
'sessions.empty': 'セッションがありません',
'sessions.unknownTime': '不明な時間',
Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/i18n/locales/vi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@ const vi = Object.freeze({
'sessions.source.claudeCode': 'Claude Code',
'sessions.source.gemini': 'Gemini CLI',
'sessions.source.codebuddy': 'CodeBuddy Code',
'sessions.source.pi': 'Pi',
'sessions.loadingList': 'Đang tải phiên làm việc...',
'sessions.empty': 'Không tìm thấy phiên nào',
'sessions.unknownTime': 'thời gian không rõ',
Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/i18n/locales/zh-tw.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ const zhTw = Object.freeze({
'sessions.source.claudeCode': 'Claude Code',
'sessions.source.gemini': 'Gemini CLI',
'sessions.source.codebuddy': 'CodeBuddy Code',
'sessions.source.pi': 'Pi',
'sessions.loadingList': '會話載入中...',
'sessions.empty': '暫無可用會話記錄',
'sessions.unknownTime': '未知時間',
Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/i18n/locales/zh.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ const zh = Object.freeze({
'sessions.source.claudeCode': 'Claude Code',
'sessions.source.gemini': 'Gemini CLI',
'sessions.source.codebuddy': 'CodeBuddy Code',
'sessions.source.pi': 'Pi',
'sessions.loadingList': '会话加载中...',
'sessions.empty': '暂无可用会话记录',
'sessions.unknownTime': '未知时间',
Expand Down
Loading