Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
A professional workspace for AI coding agents with: - Real-time streaming chat with SSE - Session management (fork, clone, tree navigation) - MCP tools browser & metadata panel - Skills browser (global, project, package sources) - Voice input & STT provider configuration - Integrated terminal & file explorer - Model selector with capability metadata - Keyboard-first UI with command palette - 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi) - Tauri 2 desktop app - React 19 + TypeScript + Vite 8 + Tailwind CSS 4 - Pi Agent extension runtime Originally forked from OpenCodeUI, now an independent project. License: GPL-3.0-only
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
/**
|
|
* Shared formatting utilities.
|
|
* Consolidated from duplicated functions across message parts, hooks, and renderers.
|
|
*/
|
|
|
|
import type { CompletedAtFormat } from '../store/themeStore'
|
|
|
|
/** Format a tool name for display: "my-tool_name" → "My Tool Name" */
|
|
export function formatToolName(name: string): string {
|
|
return name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
|
}
|
|
|
|
/** Format a duration in ms to human-readable string */
|
|
export function formatDuration(ms: number): string {
|
|
const normalizedMs = Math.max(0, Math.round(ms))
|
|
if (normalizedMs < 1000) return `${normalizedMs}ms`
|
|
const s = normalizedMs / 1000
|
|
if (s < 60) return `${s.toFixed(1)}s`
|
|
|
|
const totalSeconds = Math.round(s)
|
|
const d = Math.floor(totalSeconds / (60 * 60 * 24))
|
|
const h = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60))
|
|
const m = Math.floor((totalSeconds % (60 * 60)) / 60)
|
|
const remS = totalSeconds % 60
|
|
|
|
if (d > 0) return [`${d}d`, h > 0 ? `${h}h` : '', m > 0 ? `${m}m` : ''].filter(Boolean).join(' ')
|
|
if (h > 0) return [`${h}h`, m > 0 ? `${m}m` : '', remS > 0 ? `${remS}s` : ''].filter(Boolean).join(' ')
|
|
return [`${m}m`, remS > 0 ? `${remS}s` : ''].filter(Boolean).join(' ')
|
|
}
|
|
|
|
/** Format a cost in dollars */
|
|
export function formatCost(cost: number): string {
|
|
if (cost === 0) return '$0'
|
|
if (cost < 0.001) return '<$0.001'
|
|
if (cost < 0.01) return '$' + cost.toFixed(3)
|
|
if (cost < 1) return '$' + cost.toFixed(2)
|
|
return '$' + cost.toFixed(2)
|
|
}
|
|
|
|
/** Format a timestamp (ms) to local HH:MM time string */
|
|
export function formatTime(ms: number): string {
|
|
const date = new Date(ms)
|
|
const h = date.getHours().toString().padStart(2, '0')
|
|
const m = date.getMinutes().toString().padStart(2, '0')
|
|
return `${h}:${m}`
|
|
}
|
|
|
|
/** Format a timestamp (ms) to local YYYY-MM-DD HH:MM string */
|
|
export function formatDateTime(ms: number): string {
|
|
const date = new Date(ms)
|
|
const y = date.getFullYear().toString().padStart(4, '0')
|
|
const mon = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
const d = date.getDate().toString().padStart(2, '0')
|
|
return `${y}-${mon}-${d} ${formatTime(ms)}`
|
|
}
|
|
|
|
/** Format a timestamp (ms) to local YYYY-MM-DD HH:MM:SS string for detail tooltips */
|
|
export function formatDetailedDateTime(ms: number): string {
|
|
const date = new Date(ms)
|
|
const y = date.getFullYear().toString().padStart(4, '0')
|
|
const mon = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
const d = date.getDate().toString().padStart(2, '0')
|
|
const h = date.getHours().toString().padStart(2, '0')
|
|
const m = date.getMinutes().toString().padStart(2, '0')
|
|
const s = date.getSeconds().toString().padStart(2, '0')
|
|
return `${y}-${mon}-${d} ${h}:${m}:${s}`
|
|
}
|
|
|
|
/** Format completed time according to the selected display mode */
|
|
export function formatCompletedAt(ms: number, format: CompletedAtFormat): string {
|
|
return format === 'dateTime' ? formatDateTime(ms) : formatTime(ms)
|
|
}
|
|
|
|
/** Format a large number with k/M suffix */
|
|
export function formatNumber(num: number): string {
|
|
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
|
if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
|
|
return num.toString()
|
|
}
|