mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): make session header metadata configurable (#1267)
* feat(web): make session header metadata configurable * fix(web): align mobile header metadata priority
This commit is contained in:
@@ -18,6 +18,11 @@ describe('codexStatusLabels', () => {
|
||||
expect(formatCodexReasoningLabel('Ultra')).toBe('reasoning ultra')
|
||||
})
|
||||
|
||||
it('can omit the reasoning field label', () => {
|
||||
expect(formatCodexReasoningLabel('xhigh', false)).toBe('xhigh')
|
||||
expect(formatCodexReasoningLabel(null, false)).toBe('default')
|
||||
})
|
||||
|
||||
it('formats compact effort-only labels', () => {
|
||||
expect(formatCompactCodexReasoningLabel(null)).toBe('default')
|
||||
expect(formatCompactCodexReasoningLabel('default')).toBe('default')
|
||||
|
||||
@@ -6,8 +6,9 @@ export function formatCompactCodexReasoningLabel(effort?: string | null): string
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function formatCodexReasoningLabel(effort?: string | null): string {
|
||||
return `reasoning ${formatCompactCodexReasoningLabel(effort)}`
|
||||
export function formatCodexReasoningLabel(effort?: string | null, showLabel = true): string {
|
||||
const value = formatCompactCodexReasoningLabel(effort)
|
||||
return showLabel ? `reasoning ${value}` : value
|
||||
}
|
||||
|
||||
export function shouldShowCodexReasoningLabel(agentFlavor: string | null | undefined): boolean {
|
||||
|
||||
@@ -689,6 +689,20 @@ export default {
|
||||
'settings.display.sessionListStatus.standard': 'Standard',
|
||||
'settings.display.sessionListStatus.detailed': 'Detailed',
|
||||
'settings.display.sessionListStatus.detailedDescription': 'Shows why a session stopped: permission, input, background work, new activity, or a scheduled message (clock icon).',
|
||||
'settings.display.sessionHeader': 'Session header',
|
||||
'settings.display.sessionHeader.description': 'Choose which details appear below the session title. Mobile shows the agent plus the highest-priority available detail.',
|
||||
'settings.display.sessionHeader.showLabels': 'Show field labels',
|
||||
'settings.display.sessionHeader.agent': 'Agent',
|
||||
'settings.display.sessionHeader.model': 'Model',
|
||||
'settings.display.sessionHeader.reasoning': 'Reasoning effort',
|
||||
'settings.display.sessionHeader.fastMode': 'Fast mode',
|
||||
'settings.display.sessionHeader.machine': 'Machine',
|
||||
'settings.display.sessionHeader.lastActive': 'Active time',
|
||||
'settings.display.sessionHeader.createdAt': 'Created time',
|
||||
'settings.display.sessionHeader.updatedAt': 'Updated time',
|
||||
'settings.display.sessionHeader.worktree': 'Worktree branch',
|
||||
'session.header.createdAt': 'Created',
|
||||
'session.header.updatedAt': 'Updated',
|
||||
'settings.chat.title': 'Chat',
|
||||
'settings.chat.description': 'Message input, tool cards, and conversation colors.',
|
||||
'settings.chat.input': 'Input',
|
||||
|
||||
@@ -693,6 +693,20 @@ export default {
|
||||
'settings.display.sessionListStatus.standard': '标准',
|
||||
'settings.display.sessionListStatus.detailed': '详细',
|
||||
'settings.display.sessionListStatus.detailedDescription': '显示会话停止的原因:权限、输入、后台任务、新活动或定时消息(时钟图标)。',
|
||||
'settings.display.sessionHeader': '会话顶部信息',
|
||||
'settings.display.sessionHeader.description': '选择在会话标题下方显示的信息。移动端显示 Agent 类型及优先级最高的一个可用信息。',
|
||||
'settings.display.sessionHeader.showLabels': '显示字段标题',
|
||||
'settings.display.sessionHeader.agent': 'Agent 类型',
|
||||
'settings.display.sessionHeader.model': '模型',
|
||||
'settings.display.sessionHeader.reasoning': '推理强度',
|
||||
'settings.display.sessionHeader.fastMode': '快速模式',
|
||||
'settings.display.sessionHeader.machine': '机器',
|
||||
'settings.display.sessionHeader.lastActive': '活跃时间',
|
||||
'settings.display.sessionHeader.createdAt': '创建时间',
|
||||
'settings.display.sessionHeader.updatedAt': '更新时间',
|
||||
'settings.display.sessionHeader.worktree': '工作树分支',
|
||||
'session.header.createdAt': '创建',
|
||||
'session.header.updatedAt': '更新',
|
||||
'settings.chat.title': '聊天',
|
||||
'settings.chat.description': '消息输入、工具卡片和对话颜色。',
|
||||
'settings.chat.input': '输入',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { selectMobileSessionHeaderSecondary } from './sessionHeaderMobileMetadata'
|
||||
|
||||
describe('selectMobileSessionHeaderSecondary', () => {
|
||||
it('follows the upstream-first display order', () => {
|
||||
expect(selectMobileSessionHeaderSecondary({ machine: true, model: true, updatedAt: true })).toBe('machine')
|
||||
expect(selectMobileSessionHeaderSecondary({ model: true, updatedAt: true, worktree: true })).toBe('model')
|
||||
})
|
||||
|
||||
it('uses the next enabled and available detail when the model is absent', () => {
|
||||
expect(selectMobileSessionHeaderSecondary({ machine: true, lastActive: true, updatedAt: true })).toBe('machine')
|
||||
expect(selectMobileSessionHeaderSecondary({ lastActive: true, updatedAt: true })).toBe('lastActive')
|
||||
expect(selectMobileSessionHeaderSecondary({ fastMode: true, updatedAt: true, createdAt: true, worktree: true })).toBe('fastMode')
|
||||
expect(selectMobileSessionHeaderSecondary({ updatedAt: true, createdAt: true, worktree: true })).toBe('createdAt')
|
||||
expect(selectMobileSessionHeaderSecondary({ worktree: true, fastMode: true })).toBe('fastMode')
|
||||
})
|
||||
|
||||
it('returns no secondary detail when none are available', () => {
|
||||
expect(selectMobileSessionHeaderSecondary({})).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
export type SessionHeaderSecondaryMetadataKey =
|
||||
| 'model'
|
||||
| 'reasoning'
|
||||
| 'machine'
|
||||
| 'lastActive'
|
||||
| 'updatedAt'
|
||||
| 'createdAt'
|
||||
| 'worktree'
|
||||
| 'fastMode'
|
||||
|
||||
const MOBILE_SECONDARY_PRIORITY: ReadonlyArray<SessionHeaderSecondaryMetadataKey> = [
|
||||
'machine',
|
||||
'lastActive',
|
||||
'model',
|
||||
'reasoning',
|
||||
'fastMode',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'worktree',
|
||||
]
|
||||
|
||||
export function selectMobileSessionHeaderSecondary(
|
||||
available: Partial<Record<SessionHeaderSecondaryMetadataKey, boolean>>
|
||||
): SessionHeaderSecondaryMetadataKey | null {
|
||||
return MOBILE_SECONDARY_PRIORITY.find((key) => available[key] === true) ?? null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatSessionHeaderTimestamp } from './sessionHeaderTimestamp'
|
||||
|
||||
describe('formatSessionHeaderTimestamp', () => {
|
||||
it('formats a session timestamp in the selected locale', () => {
|
||||
const value = new Date(2026, 6, 26, 8, 53).getTime()
|
||||
expect(formatSessionHeaderTimestamp(value, 'en-US')).toMatch(/Jul 26, 2026.*08:53 AM/)
|
||||
expect(formatSessionHeaderTimestamp(value, 'zh-CN')).toContain('2026年7月26日')
|
||||
})
|
||||
|
||||
it('rejects missing and invalid timestamps', () => {
|
||||
expect(formatSessionHeaderTimestamp(0)).toBeNull()
|
||||
expect(formatSessionHeaderTimestamp(Number.NaN)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
export function formatSessionHeaderTimestamp(value: number, locale?: string): string | null {
|
||||
if (!Number.isFinite(value) || value <= 0) return null
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
Reference in New Issue
Block a user