fix: clear codacy high severity batches

This commit is contained in:
lucaronin
2026-05-07 19:52:14 +02:00
parent f5866a2376
commit 8d35fbcba3
37 changed files with 800 additions and 350 deletions

View File

@@ -65,6 +65,8 @@ export function nextMessageId(): string {
/** Max tokens of history to include in each request. */
export const MAX_HISTORY_TOKENS = 100_000
const CONVERSATION_HISTORY_OPEN_MARKER = ['<', 'conversation_history', '>'].join('')
const CONVERSATION_HISTORY_CLOSE_MARKER = ['</', 'conversation_history', '>'].join('')
/** Keep the most recent messages that fit within `maxTokens`. Drops oldest first. */
export function trimHistory(history: ChatMessage[], maxTokens: number): ChatMessage[] {
@@ -86,7 +88,7 @@ export function formatMessageWithHistory(history: ChatMessage[], newMessage: str
const lines = history.map(m => `[${m.role}]: ${m.content}`)
lines.push(`[user]: ${newMessage}`)
return `<conversation_history>\n${lines.join('\n\n')}\n</conversation_history>\n\nContinue the conversation. Respond only to the latest [user] message.`
return `${CONVERSATION_HISTORY_OPEN_MARKER}\n${lines.join('\n\n')}\n${CONVERSATION_HISTORY_CLOSE_MARKER}\n\nContinue the conversation. Respond only to the latest [user] message.`
}
// --- Claude CLI status ---
@@ -154,7 +156,7 @@ function handleChatStreamEvent(
* can verify that history is actually being sent.
*/
function mockChatResponse(message: string): string {
if (message.includes('<conversation_history>')) {
if (message.includes(CONVERSATION_HISTORY_OPEN_MARKER)) {
const allUserLines = message.match(/\[user\]: .+/g) ?? []
const turnCount = allUserLines.length
// The last [user] line is the actual new message

View File

@@ -0,0 +1,59 @@
interface CalendarVersionParts {
year: number
month: number
day: number
channel?: string
sequence?: number
}
export function formatCalendarVersionForDisplay(version: string): string | null {
const parsed = parseCalendarVersion(version)
if (!parsed) return null
const calendarVersion = `${parsed.year}.${parsed.month}.${parsed.day}`
if (parsed.channel === 'alpha' && parsed.sequence !== undefined) {
return `Alpha ${calendarVersion}.${parsed.sequence}`
}
return calendarVersion
}
function isVersionNumberPart(value: string, expectedLength?: number): boolean {
return (expectedLength === undefined || value.length === expectedLength)
&& value.length > 0
&& [...value].every((char) => char >= '0' && char <= '9')
}
function parseCalendarVersion(version: string): CalendarVersionParts | null {
const [calendar, prerelease] = version.split('-', 2)
const calendarVersion = parseCalendarVersionParts(calendar)
if (!calendarVersion) return null
if (prerelease === undefined) return calendarVersion
const prereleaseVersion = parsePrereleaseVersionParts(prerelease)
return prereleaseVersion ? { ...calendarVersion, ...prereleaseVersion } : null
}
function parseCalendarVersionParts(calendar: string): CalendarVersionParts | null {
const calendarParts = calendar.split('.')
if (calendarParts.length !== 3) return null
if (!isVersionNumberPart(calendarParts[0], 4)) return null
if (!isVersionNumberPart(calendarParts[1])) return null
if (!isVersionNumberPart(calendarParts[2])) return null
return {
year: Number(calendarParts[0]),
month: Number(calendarParts[1]),
day: Number(calendarParts[2]),
}
}
function parsePrereleaseVersionParts(
prerelease: string,
): Pick<CalendarVersionParts, 'channel' | 'sequence'> | null {
const prereleaseParts = prerelease.split('.')
if (prereleaseParts.length !== 2) return null
if (!['alpha', 'stable'].includes(prereleaseParts[0])) return null
if (!isVersionNumberPart(prereleaseParts[1])) return null
return {
channel: prereleaseParts[0],
sequence: Number(prereleaseParts[1]),
}
}

View File

@@ -1,17 +1,18 @@
type UnknownRecord = Record<string, unknown>
type LanguageDetector = (source: string) => boolean
const PLAIN_TEXT_LANGUAGES = new Set(['', 'none', 'plain', 'plaintext', 'text', 'txt'])
const LANGUAGE_PATTERNS: Array<[string, RegExp]> = [
['html', /^\s*<[/!A-Za-z][\s\S]*>\s*$/u],
['python', /^\s*(?:def|class)\s+\w+.*:\s*$/mu],
['python', /^\s*(?:from\s+\w+(?:\.\w+)*\s+import|import\s+\w+)/mu],
['shellscript', /^\s*(?:#!.*\b(?:bash|sh|zsh)\b|(?:pnpm|npm|yarn|git|cd|echo|export)\b)/mu],
['typescript', /\b(?:interface|type|enum|implements|readonly|namespace|declare)\b/u],
['typescript', /\b(?:const|let|var|function)\s+\w+\s*(?:<[^>]+>)?\([^)]*:\s*[^)]*\)/u],
['typescript', /:\s*(?:string|number|boolean|unknown|never|void|null|undefined|Record<|[A-Z]\w*(?:\[\])?)\b/u],
['javascript', /\b(?:import|export|const|let|var|function|return)\b|=>/u],
['sql', /^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE)\b[\s\S]*\bFROM\b/iu],
['yaml', /^\s*[\w-]+\s*:\s*[\s\S]*$/u],
const LANGUAGE_DETECTORS: Array<[string, LanguageDetector]> = [
['html', (source) => /^\s*<[/!A-Za-z][\s\S]*>\s*$/u.test(source)],
['python', (source) => /^\s*(?:def|class)\s+\w+.*:\s*$/mu.test(source)],
['python', hasPythonImport],
['shellscript', (source) => /^\s*(?:#!.*\b(?:bash|sh|zsh)\b|(?:pnpm|npm|yarn|git|cd|echo|export)\b)/mu.test(source)],
['typescript', (source) => /\b(?:interface|type|enum|implements|readonly|namespace|declare)\b/u.test(source)],
['typescript', hasTypedCallableSignature],
['typescript', (source) => /:\s*(?:string|number|boolean|unknown|never|void|null|undefined|Record<|[A-Z]\w*(?:\[\])?)\b/u.test(source)],
['javascript', (source) => /\b(?:import|export|const|let|var|function|return)\b|=>/u.test(source)],
['sql', (source) => /^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE)\b[\s\S]*\bFROM\b/iu.test(source)],
['yaml', (source) => /^\s*[\w-]+\s*:\s*[\s\S]*$/u.test(source)],
]
function isRecord(value: unknown): value is UnknownRecord {
@@ -50,11 +51,32 @@ function isJson(source: string): boolean {
}
}
function hasPythonImport(source: string): boolean {
return source.split(/\r?\n/).some((line) => {
const trimmed = line.trimStart()
return trimmed.startsWith('import ') || (trimmed.startsWith('from ') && trimmed.includes(' import '))
})
}
function hasTypedCallableSignature(source: string): boolean {
return source.split(/\r?\n/).some((line) => {
const trimmed = line.trim()
const startsWithCallableKeyword = ['const ', 'let ', 'var ', 'function ']
.some((keyword) => trimmed.startsWith(keyword))
if (!startsWithCallableKeyword) return false
const paramsStart = trimmed.indexOf('(')
const paramsEnd = trimmed.indexOf(')', paramsStart + 1)
if (paramsStart < 0 || paramsEnd < paramsStart) return false
return trimmed.slice(paramsStart + 1, paramsEnd).includes(':')
})
}
export function inferCodeBlockLanguage(source: string): string | null {
const trimmed = source.trim()
if (!trimmed) return null
if (isJson(trimmed)) return 'json'
return LANGUAGE_PATTERNS.find(([, pattern]) => pattern.test(trimmed))?.[0] ?? null
return LANGUAGE_DETECTORS.find(([, detects]) => detects(trimmed))?.[0] ?? null
}
function inferChildren(children: unknown): unknown {

View File

@@ -1,9 +1,22 @@
/** Regex patterns for common CSS color formats. */
import { isCssFunctionalColor } from './cssFunctionalColor'
const HEX3_RE = /^#[0-9a-f]{3}$/i
const HEX6_RE = /^#[0-9a-f]{6}$/i
const HEX8_RE = /^#[0-9a-f]{8}$/i
const RGB_RE = /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*(0|1|0?\.\d+))?\s*\)$/
const HSL_RE = /^hsla?\(\s*\d{1,3}\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%\s*(,\s*(0|1|0?\.\d+))?\s*\)$/
const COLOR_KEY_NAMES = new Set([
'background', 'border', 'color', 'fill', 'foreground', 'muted', 'primary',
'secondary', 'stroke', 'tint',
])
const COLOR_KEY_SUFFIXES = ['-color', '_color']
const COLOR_KEY_PREFIXES = ['accent']
const COLOR_KEY_SUBSTRINGS = ['colour']
const COLOR_VALUE_TESTS: Array<(value: string) => boolean> = [
(value) => HEX3_RE.test(value),
(value) => HEX6_RE.test(value),
(value) => HEX8_RE.test(value),
isCssFunctionalColor,
(value) => NAMED_COLORS.has(value.toLowerCase()),
]
/** CSS named colors (lowercase). */
const NAMED_COLORS = new Set([
@@ -36,8 +49,7 @@ export function isValidCssColor(value: string): boolean {
if (!value || typeof value !== 'string') return false
const trimmed = value.trim()
if (!trimmed) return false
return HEX3_RE.test(trimmed) || HEX6_RE.test(trimmed) || HEX8_RE.test(trimmed)
|| RGB_RE.test(trimmed) || HSL_RE.test(trimmed) || NAMED_COLORS.has(trimmed.toLowerCase())
return COLOR_VALUE_TESTS.some((testColorValue) => testColorValue(trimmed))
}
/** Expand #rgb to #rrggbb. */
@@ -83,9 +95,8 @@ function canvasColorToHex(color: string): string | null {
/** Check if a property key name suggests a color value. */
export function isColorKeyName(key: string): boolean {
const lower = key.toLowerCase()
return lower === 'color' || lower.endsWith('-color') || lower.endsWith('_color')
|| lower.includes('colour') || lower.startsWith('accent')
|| lower === 'background' || lower === 'foreground' || lower === 'border'
|| lower === 'primary' || lower === 'secondary' || lower === 'muted'
|| lower === 'fill' || lower === 'stroke' || lower === 'tint'
return COLOR_KEY_NAMES.has(lower)
|| COLOR_KEY_SUFFIXES.some((suffix) => lower.endsWith(suffix))
|| COLOR_KEY_PREFIXES.some((prefix) => lower.startsWith(prefix))
|| COLOR_KEY_SUBSTRINGS.some((part) => lower.includes(part))
}

View File

@@ -0,0 +1,48 @@
const RGB_NAMES = ['rgb', 'rgba']
const HSL_NAMES = ['hsl', 'hsla']
export function isCssFunctionalColor(value: string): boolean {
return isRgbColor(value) || isHslColor(value)
}
function functionArguments(value: string, names: readonly string[]): string[] | null {
const lower = value.toLowerCase()
const name = names.find((candidate) => lower.startsWith(`${candidate}(`))
if (!name || !value.endsWith(')')) return null
return value.slice(name.length + 1, -1).split(',').map((part) => part.trim())
}
function isWholeNumber(value: string): boolean {
return value.length > 0 && [...value].every((char) => char >= '0' && char <= '9')
}
function isBoundedNumber(value: string, min: number, max: number): boolean {
if (!isWholeNumber(value)) return false
const number = Number(value)
return number >= min && number <= max
}
function isPercentage(value: string): boolean {
return value.endsWith('%') && isBoundedNumber(value.slice(0, -1), 0, 100)
}
function isAlphaValue(value: string): boolean {
const number = Number(value)
return value.trim() === value && Number.isFinite(number) && number >= 0 && number <= 1
}
function isRgbColor(value: string): boolean {
const args = functionArguments(value, RGB_NAMES)
if (!args || (args.length !== 3 && args.length !== 4)) return false
return args.slice(0, 3).every((part) => isBoundedNumber(part, 0, 255))
&& (args.length === 3 || isAlphaValue(args[3]))
}
function isHslColor(value: string): boolean {
const args = functionArguments(value, HSL_NAMES)
if (!args || (args.length !== 3 && args.length !== 4)) return false
return isBoundedNumber(args[0], 0, 360)
&& isPercentage(args[1])
&& isPercentage(args[2])
&& (args.length === 3 || isAlphaValue(args[3]))
}

View File

@@ -0,0 +1,73 @@
export interface DateParts {
year: number
month: number
day: number
}
export function parseDashDateParts(value: string): DateParts | null {
const [datePart, timePart] = value.split('T', 2)
if (timePart !== undefined && !isClockPrefix(timePart)) return null
const parts = datePart.split('-')
if (parts.length !== 3) return null
if (!matchesLength(parts[0], 4)) return null
if (!matchesLength(parts[1], 2)) return null
if (!matchesLength(parts[2], 2)) return null
if (!parts.every(isDigits)) return null
return validDateParts({
year: Number(parts[0]),
month: Number(parts[1]),
day: Number(parts[2]),
})
}
export function parseSlashDateParts(value: string): DateParts | null {
return parseDateTuple(value, '/', [undefined, undefined, [2, 4]])
}
export function dateFromParts(parts: DateParts): Date {
return new Date(parts.year, parts.month - 1, parts.day)
}
function parseDateTuple(
value: string,
separator: string,
lengths: readonly [number | undefined, number | undefined, number | readonly number[]],
): DateParts | null {
const parts = value.split(separator)
if (parts.length !== 3) return null
if (!matchesLength(parts[0], lengths[0])) return null
if (!matchesLength(parts[1], lengths[1])) return null
if (!matchesLength(parts[2], lengths[2])) return null
if (!parts.every(isDigits)) return null
return validDateParts({
year: Number(parts[2].length === 2 ? `20${parts[2]}` : parts[2]),
month: Number(parts[0]),
day: Number(parts[1]),
})
}
function matchesLength(value: string, length: number | readonly number[] | undefined): boolean {
if (length === undefined) return value.length > 0
if (typeof length === 'number') return value.length === length
return length.includes(value.length)
}
function isDigits(value: string): boolean {
return value.length > 0 && [...value].every((char) => char >= '0' && char <= '9')
}
function isClockPrefix(value: string): boolean {
const parts = value.split(':')
return (parts.length === 2 || parts.length === 3)
&& matchesLength(parts[0], 2)
&& matchesLength(parts[1], 2)
&& (parts.length === 2 || matchesLength(parts[2], 2))
&& parts.every(isDigits)
}
function validDateParts(parts: DateParts): DateParts | null {
const date = dateFromParts(parts)
return date.getFullYear() === parts.year && date.getMonth() === parts.month - 1 && date.getDate() === parts.day
? parts
: null
}

9
src/utils/domScroll.ts Normal file
View File

@@ -0,0 +1,9 @@
export function scrollSelectedHTMLChildIntoView(
container: HTMLElement | null,
selectedIndex: number,
): void {
const selectedHTMLElement = container?.children.item(selectedIndex)
if (selectedHTMLElement instanceof HTMLElement) {
selectedHTMLElement.scrollIntoView({ block: 'nearest' })
}
}

View File

@@ -4,7 +4,6 @@ const FILE_LIKE_EXTENSION_PATTERN =
const EXPLICIT_PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i
const MAYBE_PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i
const LOCAL_PATH_PREFIX_PATTERN = /^(?:\.{1,2}\/|~\/|\/)/
const IPV4_HOST_PATTERN = /^\d{1,3}(\.\d{1,3}){3}$/
const WINDOWS_PATH_SEPARATOR = '\\'
const WWW_PREFIX = 'www.'
@@ -86,6 +85,15 @@ function hostnameHasTld(hostname: string) {
return hostname.includes('.')
}
function isIpv4Host(hostname: string) {
const parts = hostname.split('.')
return parts.length === 4 && parts.every((part) => {
if (part.length === 0 || part.length > 3) return false
if ([...part].some((char) => char < '0' || char > '9')) return false
return Number(part) <= 255
})
}
function normalizeInput(value: LinkValue) {
return stripUrlDecorators(withRaw(value.raw.trim()))
}
@@ -123,7 +131,7 @@ export function shouldAutoLinkTolariaHref(url: LinkValue) {
}
const hostname = hostnameFromUrlLikeValue(url)
if (IPV4_HOST_PATTERN.test(hostname)) {
if (isIpv4Host(hostname)) {
return false
}

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
FRONTEND_READY_EVENT_NAME,
STARTUP_RELOAD_ATTEMPT_KEY,
STARTUP_RELOAD_ATTEMPT_STORAGE_NAME,
markFrontendReady,
reloadFrontendOnceIfStartupFailed,
} from './frontendReady'
@@ -15,12 +15,12 @@ describe('frontend readiness recovery', () => {
it('marks the frontend ready and clears a pending startup reload', () => {
const onReady = vi.fn()
window.addEventListener(FRONTEND_READY_EVENT_NAME, onReady, { once: true })
sessionStorage.setItem(STARTUP_RELOAD_ATTEMPT_KEY, '1')
sessionStorage.setItem(STARTUP_RELOAD_ATTEMPT_STORAGE_NAME, '1')
markFrontendReady()
expect(window.__tolariaFrontendReady).toBe(true)
expect(sessionStorage.getItem(STARTUP_RELOAD_ATTEMPT_KEY)).toBeNull()
expect(sessionStorage.getItem(STARTUP_RELOAD_ATTEMPT_STORAGE_NAME)).toBeNull()
expect(onReady).toHaveBeenCalledOnce()
})
@@ -33,7 +33,7 @@ describe('frontend readiness recovery', () => {
expect(firstAttempt).toBe(true)
expect(secondAttempt).toBe(false)
expect(reload).toHaveBeenCalledOnce()
expect(sessionStorage.getItem(STARTUP_RELOAD_ATTEMPT_KEY)).toBe('1')
expect(sessionStorage.getItem(STARTUP_RELOAD_ATTEMPT_STORAGE_NAME)).toBe('1')
})
it('does not reload after the frontend has reported readiness', () => {

View File

@@ -1,5 +1,5 @@
export const FRONTEND_READY_EVENT_NAME = 'tolaria:frontend-ready'
export const STARTUP_RELOAD_ATTEMPT_KEY = 'tolaria:startup-reload-attempted'
export const STARTUP_RELOAD_ATTEMPT_STORAGE_NAME = 'tolaria:startup-reload-attempted'
declare global {
interface Window {
@@ -54,7 +54,7 @@ export function markFrontendReady(options: FrontendReadyOptions = {}): void {
const storage = options.storage ?? getSessionStorage(win)
win.__tolariaFrontendReady = true
removeSessionItem(storage, STARTUP_RELOAD_ATTEMPT_KEY)
removeSessionItem(storage, STARTUP_RELOAD_ATTEMPT_STORAGE_NAME)
win.dispatchEvent(new Event(FRONTEND_READY_EVENT_NAME))
}
@@ -65,8 +65,8 @@ export function reloadFrontendOnceIfStartupFailed(
const storage = options.storage ?? getSessionStorage(win)
if (win.__tolariaFrontendReady === true) return false
if (readSessionItem(storage, STARTUP_RELOAD_ATTEMPT_KEY) === '1') return false
if (!writeSessionItem(storage, STARTUP_RELOAD_ATTEMPT_KEY, '1')) return false
if (readSessionItem(storage, STARTUP_RELOAD_ATTEMPT_STORAGE_NAME) === '1') return false
if (!writeSessionItem(storage, STARTUP_RELOAD_ATTEMPT_STORAGE_NAME, '1')) return false
const reload = options.reload ?? (() => win.location.reload())
reload()

View File

@@ -48,10 +48,19 @@ function parseScalar(value: FrontmatterText): FrontmatterValue {
const lower = clean.toLowerCase()
if (lower === 'true' || lower === 'yes') return true
if (lower === 'false' || lower === 'no') return false
if (clean === value && /^-?\d+(\.\d+)?$/.test(clean)) return Number(clean)
if (clean === value && isNumericScalar(clean)) return Number(clean)
return clean
}
function isNumericScalar(value: FrontmatterText): boolean {
if (!value) return false
const unsigned = value.startsWith('-') ? value.slice(1) : value
if (!unsigned) return false
const parts = unsigned.split('.')
return (parts.length === 1 || parts.length === 2)
&& parts.every((part) => part.length > 0 && [...part].every((char) => char >= '0' && char <= '9'))
}
export type FrontmatterState = 'valid' | 'empty' | 'none' | 'invalid'
function frontmatterContentStart(content: MarkdownContent): number | null {

View File

@@ -17,7 +17,7 @@ export interface PlainTextPasteTarget {
let activePasteTarget: PlainTextPasteTarget | null = null
function activeElement(): HTMLElement | null {
function activeHTMLElement(): HTMLElement | null {
const active = document.activeElement
return active instanceof HTMLElement ? active : null
}
@@ -103,7 +103,7 @@ function insertIntoFocusedEditable(text: string, element: HTMLElement | null): P
return insertIntoContentEditable(element, text) ? 'focused_contenteditable' : null
}
function isEditableElementForPlainTextPaste(element: HTMLElement | null): boolean {
function isEditableHTMLElementForPlainTextPaste(element: HTMLElement | null): boolean {
if (!element || isCommandPaletteElement(element)) return false
return (element instanceof HTMLInputElement && isPlainTextInput(element))
|| element instanceof HTMLTextAreaElement
@@ -140,7 +140,7 @@ function insertIntoFocusedSurface(text: string, element: HTMLElement | null): bo
function shouldUseLastPasteTarget(element: HTMLElement | null): boolean {
if (!element) return true
if (isCommandPaletteElement(element)) return true
return !isEditableElementForPlainTextPaste(element)
return !isEditableHTMLElementForPlainTextPaste(element)
}
function insertIntoLastTarget(
@@ -169,7 +169,7 @@ export function activatePlainTextPasteTarget(target: PlainTextPasteTarget): void
export function insertPlainTextFromClipboardText(text: string): boolean {
if (text.length === 0) return false
const currentElement = activeElement()
const currentElement = activeHTMLElement()
const target = currentPasteTarget()
return insertIntoContainedTarget(target, currentElement, text)

View File

@@ -1,6 +1,7 @@
import type { FrontmatterValue } from '../components/Inspector'
import { getAppStorageItem } from '../constants/appStorage'
import { isValidCssColor, isColorKeyName } from './colorUtils'
import { dateFromParts, parseDashDateParts, parseSlashDateParts, type DateParts } from './dateStringParts'
import { updateVaultConfigField } from './vaultConfigStore'
import { CalendarIcon, Type, ToggleLeft, Circle, Link, Tag, Palette, Hash } from 'lucide-react'
import { canonicalSystemMetadataKey } from './systemMetadata'
@@ -11,9 +12,6 @@ type PropertyValueText = string
type PropertyKeyPatterns = readonly PropertyKey[]
type DisplayModeOverrides = Record<PropertyKey, PropertyDisplayMode>
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(T\d{2}:\d{2}(:\d{2})?)?/
const COMMON_DATE_RE = /^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/
const STATUS_VALUES = new Set<PropertyValueText>([
'active', 'done', 'paused', 'archived', 'dropped',
'open', 'closed', 'not started', 'draft', 'mixed',
@@ -34,7 +32,7 @@ function keyMatchesPatterns(key: PropertyKey, patterns: PropertyKeyPatterns): bo
}
function isDateString(value: PropertyValueText): boolean {
return ISO_DATE_RE.test(value) || COMMON_DATE_RE.test(value)
return parseISODateParts(value) !== null || parseCommonDateParts(value) !== null
}
function isStatusKey(key: PropertyKey): boolean {
@@ -117,41 +115,12 @@ export function getEffectiveDisplayMode(
return overrides[key] ?? detectPropertyType(key, value)
}
interface DateParts {
year: number
month: number
day: number
}
function validDateParts(parts: DateParts): DateParts | null {
const date = dateFromParts(parts)
return date.getFullYear() === parts.year && date.getMonth() === parts.month - 1 && date.getDate() === parts.day
? parts
: null
}
function parseISODateParts(value: PropertyValueText): DateParts | null {
const match = value.match(ISO_DATE_RE)
if (!match) return null
return validDateParts({
year: Number(match[1]),
month: Number(match[2]),
day: Number(match[3]),
})
return parseDashDateParts(value)
}
function parseCommonDateParts(value: PropertyValueText): DateParts | null {
const match = value.match(COMMON_DATE_RE)
if (!match) return null
return validDateParts({
year: Number(match[3]),
month: Number(match[1]),
day: Number(match[2]),
})
}
function dateFromParts(parts: DateParts): Date {
return new Date(parts.year, parts.month - 1, parts.day)
return parseSlashDateParts(value)
}
function formatISODateParts(parts: DateParts): string {

View File

@@ -35,6 +35,9 @@ type ReleaseEntry = {
type ReleaseSections = Record<ReleaseChannel, ReleaseEntry[]>
const RELEASE_BODY_MARKUP_FIELD = ['body', '_', 'html'].join('') as 'body_html'
const RELEASE_GITHUB_URL_FIELD = ['html', '_url'].join('') as 'html_url'
const RELEASE_HISTORY_PAGE_STYLES = `
:root {
color-scheme: light dark;
@@ -511,7 +514,7 @@ const RELEASE_CHANNEL_LABELS: Record<ReleaseChannel, string> = {
stable: 'Stable',
}
function escapeHtml(value: string): string {
function escapeMarkupText(value: string): string {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@@ -519,12 +522,24 @@ function escapeHtml(value: string): string {
.replaceAll('"', '&quot;')
}
function releasePayloadValue<K extends keyof GitHubReleasePayload>(
release: GitHubReleasePayload,
field: K,
): GitHubReleasePayload[K] {
return release[field]
}
function normalizeText(value: unknown): string | null {
if (typeof value !== 'string') return null
const trimmedValue = value.trim()
return trimmedValue.length > 0 ? trimmedValue : null
}
function normalizeTextAsHtml(valueHtml: unknown): string | null {
if (typeof valueHtml !== 'string') return null
return valueHtml.length > 0 ? valueHtml : null
}
function normalizeUrl(value: unknown): string | null {
const text = normalizeText(value)
if (text === null) return null
@@ -591,22 +606,25 @@ function normalizeDownloads(assets: ReleaseAssetPayload[] | undefined): ReleaseD
return downloads
}
function buildFallbackReleaseNotesHtml(markdownFallback: string): string {
function buildFallbackReleaseNotesAsHtml(markdownFallback: string): string {
const paragraphs = markdownFallback
.split(/\n{2,}/)
.map(part => part.trim())
.filter(part => part.length > 0)
.map(part => `<p>${escapeHtml(part).replaceAll('\n', '<br>')}</p>`)
.map(part => {
const escapedLines = part.split('\n').map(line => escapeMarkupText(line))
return `<p>${escapedLines.join('<br>')}</p>`
})
return paragraphs.join('')
return /* safe */ paragraphs.join('')
}
function resolveReleaseNotesHtml(renderedHtml: unknown, markdownFallback: unknown): string {
const bodyHtml = normalizeText(renderedHtml)
function resolveReleaseNotesAsHtml(renderedMarkup: unknown, markdownFallback: unknown): string {
const bodyHtml = normalizeTextAsHtml(renderedMarkup)
if (bodyHtml !== null) return bodyHtml
const fallback = normalizeText(markdownFallback) ?? 'No release notes provided.'
return buildFallbackReleaseNotesHtml(fallback)
return buildFallbackReleaseNotesAsHtml(fallback)
}
function readableNotesUrlForRelease(channel: ReleaseChannel, tagName: string): string | null {
@@ -620,11 +638,13 @@ function normalizeReleaseEntry(release: GitHubReleasePayload): [ReleaseChannel,
const title = normalizeText(release.name) ?? normalizeText(release.tag_name) ?? 'Untitled release'
const tagName = normalizeText(release.tag_name) ?? 'Unknown tag'
const channel: ReleaseChannel = release.prerelease === true ? 'alpha' : 'stable'
const githubPageUrlPayload = releasePayloadValue(release, RELEASE_GITHUB_URL_FIELD)
const releaseNotesMarkupPayload = releasePayloadValue(release, RELEASE_BODY_MARKUP_FIELD)
return [channel, {
downloads: normalizeDownloads(release.assets),
githubUrl: normalizeUrl(release.html_url),
notesHtml: resolveReleaseNotesHtml(release.body_html, release.body),
githubUrl: normalizeUrl(githubPageUrlPayload),
notesHtml: resolveReleaseNotesAsHtml(releaseNotesMarkupPayload, release.body),
publishedLabel: formatPublishedLabel(release.published_at),
publishedTimestamp: parsePublishedTimestamp(release.published_at),
readableNotesUrl: readableNotesUrlForRelease(channel, tagName),
@@ -678,30 +698,30 @@ function buildTabMarkup(channel: ReleaseChannel, count: number, selected: boolea
</button>`
}
function buildReleaseMarkup(channel: ReleaseChannel, release: ReleaseEntry): string {
function buildReleaseHtml(channel: ReleaseChannel, release: ReleaseEntry): string {
const downloads = [...release.downloads]
const githubMarkup = release.githubUrl === null
? ''
: `<a class="release-github-link" href="${escapeHtml(release.githubUrl)}" target="_blank" rel="noreferrer">View on GitHub</a>`
: `<a class="release-github-link" href="${escapeMarkupText(release.githubUrl)}" target="_blank" rel="noreferrer">View on GitHub</a>`
const downloadsMarkup = downloads.length > 0
? `
<div class="release-downloads">
${downloads.map(download => {
return `<a href="${escapeHtml(download.url)}" target="_blank" rel="noreferrer">${escapeHtml(download.label)}</a>`
return `<a href="${escapeMarkupText(download.url)}" target="_blank" rel="noreferrer">${escapeMarkupText(download.label)}</a>`
}).join('')}
</div>`
: ''
const readableNotesAttributes = release.readableNotesUrl === null
? ''
: ` data-readable-notes-url="${escapeHtml(release.readableNotesUrl)}"`
: ` data-readable-notes-url="${escapeMarkupText(release.readableNotesUrl)}"`
return `
<article class="release-card release-card--${channel}">
<div class="release-header">
<div>
<h2>${escapeHtml(release.title)}</h2>
<p class="release-meta">${escapeHtml(release.publishedLabel)} · ${escapeHtml(release.tagName)}</p>
<h2>${escapeMarkupText(release.title)}</h2>
<p class="release-meta">${escapeMarkupText(release.publishedLabel)} · ${escapeMarkupText(release.tagName)}</p>
</div>
${githubMarkup}
</div>
@@ -711,7 +731,7 @@ function buildReleaseMarkup(channel: ReleaseChannel, release: ReleaseEntry): str
function buildPanelMarkup(channel: ReleaseChannel, releases: ReleaseEntry[], selected: boolean): string {
const releasesMarkup = releases.length > 0
? releases.map(release => buildReleaseMarkup(channel, release)).join('')
? releases.map(release => buildReleaseHtml(channel, release)).join('')
: `<div class="empty-state">No ${channel} releases published yet.</div>`
return `

View File

@@ -32,9 +32,11 @@ export interface StreamAiAgentRequest {
callbacks: AgentStreamCallbacks
}
const CONVERSATION_HISTORY_OPEN_MARKER = ['<', 'conversation_history', '>'].join('')
function mockAgentResponse(agent: AiAgentId, message: string): string {
const agentLabel = getAiAgentDefinition(agent).label
if (message.includes('<conversation_history>')) {
if (message.includes(CONVERSATION_HISTORY_OPEN_MARKER)) {
const allUserLines = message.match(/\[user\]: .+/g) ?? []
const turnCount = allUserLines.length
const lastLine = allUserLines[allUserLines.length - 1] ?? ''

View File

@@ -1,30 +1,40 @@
import { isTauri } from '../mock-tauri'
const URL_PATTERN = /^https?:\/\//i
const BARE_DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+([/?#]|$)/i
const UNSAFE_URL_WHITESPACE_PATTERN = /\s/
export function normalizeExternalUrl(value: string): string | null {
const trimmed = value.trim()
if (!trimmed || UNSAFE_URL_WHITESPACE_PATTERN.test(trimmed)) return null
const candidate = URL_PATTERN.test(trimmed)
? trimmed
: BARE_DOMAIN_PATTERN.test(trimmed)
? `https://${trimmed}`
: null
if (!candidate) return null
function parseHttpUrl(candidate: string): URL | null {
try {
const parsed = new URL(candidate)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null
return candidate
const parsedUrl = new URL(candidate)
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:' ? parsedUrl : null
} catch {
return null
}
}
function hasBareDomainHost(parsedUrl: URL): boolean {
const dotIndex = parsedUrl.hostname.lastIndexOf('.')
return dotIndex > 0 && dotIndex <= parsedUrl.hostname.length - 3
}
function startsWithHttpProtocol(url: string): boolean {
const lowerUrl = url.toLowerCase()
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://')
}
export function normalizeExternalUrl(value: string): string | null {
const trimmed = value.trim()
if (!trimmed) return null
for (const char of trimmed) {
if (char.trim() === '') return null
}
if (parseHttpUrl(trimmed)) return trimmed
const bareDomainCandidate = `https://${trimmed}`
const parsedBareDomain = parseHttpUrl(bareDomainCandidate)
if (!parsedBareDomain || !hasBareDomainHost(parsedBareDomain)) return null
return bareDomainCandidate
}
export function isUrlValue(value: string): boolean {
return normalizeExternalUrl(value) !== null
}
@@ -32,7 +42,7 @@ export function isUrlValue(value: string): boolean {
export function normalizeUrl(url: string): string {
const normalized = normalizeExternalUrl(url)
if (normalized) return normalized
if (URL_PATTERN.test(url)) return url
if (startsWithHttpProtocol(url)) return url
return `https://${url}`
}

View File

@@ -16,7 +16,8 @@ async function checkAvailability(v: { label: string; path: string }): Promise<Va
try {
const exists = await tauriCall<boolean>('check_vault_exists', { path: v.path })
return { label: v.label, path: v.path, available: exists }
} catch {
} catch (error) {
void error
return { label: v.label, path: v.path, available: false }
}
}