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
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@
"./locales/shell-copy.js": 1
},
"importSpecifiers": 1,
"nonTriviaTokens": 612
"nonTriviaTokens": 610
},
"src/renderer/app-shell-copy.ts": {
"importDeclarations": 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,17 @@ test('targets manual diagnostics to the current task or new-task Host profile',
});

test('presents every successful-frame context compaction outcome', () => {
assert.deepEqual(contextCompactionNotice({ kind: 'compacted', checkpointId: 'checkpoint-1' }), {
assert.deepEqual(contextCompactionNotice({ kind: 'compacted', checkpointId: 'checkpoint-1' }, 'en'), {
level: 'success',
title: 'Context compacted',
description: 'Older context was replaced with a checkpoint summary.',
});
assert.deepEqual(contextCompactionNotice({ kind: 'unchanged', reason: 'already_compacted' }), {
assert.deepEqual(contextCompactionNotice({ kind: 'unchanged', reason: 'already_compacted' }, 'en'), {
level: 'info',
title: 'Nothing to compact',
description: 'The task already uses the latest checkpoint.',
});
assert.deepEqual(contextCompactionNotice({ kind: 'failed', reason: 'write_failed' }), {
assert.deepEqual(contextCompactionNotice({ kind: 'failed', reason: 'write_failed' }, 'en'), {
level: 'error',
title: 'Compaction failed',
description: 'The task could not be compacted. Try again later.',
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/src/main/__tests__/attachment-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ describe('attachment preflight (before session create)', () => {
size: 100,
source: { type: 'file' as const, file: { size: 100 } },
}));
assert.throws(() => preflightAttachmentItems(items), /8/);
assert.throws(() => preflightAttachmentItems(items, 'zh-CN'), /8/);
});

test('rejects an oversized File so no empty session is created', () => {
assert.throws(
() => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'file', file: { size: CAP + 1 } } }]),
() => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'file', file: { size: CAP + 1 } } }], 'zh-CN'),
/50MB/,
);
});

test('rejects an oversized approval-token attachment by pending size', () => {
assert.throws(
() => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'approval', approvalId: 'a1' } }]),
() => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'approval', approvalId: 'a1' } }], 'zh-CN'),
/50MB/,
);
});
Expand All @@ -52,17 +52,25 @@ describe('attachment preflight (before session create)', () => {
preflightAttachmentItems([
{ size: 10, source: { type: 'approval', approvalId: 'dup' } },
{ size: 10, source: { type: 'approval', approvalId: 'dup' } },
]),
], 'zh-CN'),
/重复/,
);
assert.throws(
() =>
preflightAttachmentItems([
{ size: 10, source: { type: 'approval', approvalId: 'dup' } },
{ size: 10, source: { type: 'approval', approvalId: 'dup' } },
], 'en'),
/already added/,
);
});

test('passes approval tokens and files under the cap', () => {
assert.doesNotThrow(() =>
preflightAttachmentItems([
{ size: 100, source: { type: 'approval', approvalId: 'a1' } },
{ size: 100, source: { type: 'file', file: { size: 100 } } },
]),
], 'zh-CN'),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ describe('renderConversationMarkdown', () => {
modelId: 'fake',
},
];
const md = renderConversationMarkdown('skill session', messages);
const md = renderConversationMarkdown('skill session', messages, 'zh-CN');
assert.match(md, /## 你/);
assert.ok(md.includes(typed), 'export shows the typed prompt');
assert.ok(!md.includes('<invoked-skill'), 'export must not include the skill envelope');
assert.ok(!md.includes('Secret skill body'), 'export must not include skill body');
assert.match(renderConversationMarkdown('skill session', messages, 'en'), /## You/);
});
});
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/interrupted-resume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('latest interrupted resume candidate', () => {
content: { kind: 'text', text: 'ok' },
},
{ type: 'tool_call', id: 'call-2', turnId: 'turn-1', ts: 5, toolName: 'Read', args: {} },
]);
], 'en');

assert.deepEqual(turn?.tools.map((tool) => tool.status), ['completed', 'interrupted']);
assert.equal(latestInterruptedResumeTurnId(turn ? [turn] : []), undefined);
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ describe('model catalog picker helpers', () => {
}),
],
'',
'zh-CN',
);
const keys = options.map(([key]) => key);
assert.ok(
Expand All @@ -198,4 +199,9 @@ describe('model catalog picker helpers', () => {
`unsupported Codex model was offered: ${JSON.stringify(keys)}`,
);
});

it('labels a saved-but-unavailable selection in the UI locale', () => {
const [, label] = buildCatalogDailyReviewModelOptions([], 'codex::gone', 'en').at(-1)!;
assert.equal(label, 'gone · codex · Currently unavailable');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import { describeTurnErrorClass } from '../../renderer/session-status-presentati

describe('provider capacity presentation', () => {
it('uses capacity-specific copy instead of the unknown error fallback', () => {
assert.match(describeSessionErrorReason('provider_capacity') ?? '', /满载/);
assert.match(describeTurnErrorClass('provider_capacity'), /满载/);
assert.match(describeSessionErrorReason('provider_capacity', 'zh-CN') ?? '', /满载/);
assert.match(describeSessionErrorReason('provider_capacity', 'en') ?? '', /at capacity/);
assert.match(describeTurnErrorClass('provider_capacity', 'zh-CN'), /满载/);
assert.match(describeTurnErrorClass('provider_capacity', 'en'), /at capacity/);
});

it('does not recommend an immediate direct retry', () => {
const label = describeTurnErrorClass('provider_capacity');
const label = describeTurnErrorClass('provider_capacity', 'zh-CN');
assert.match(label, /等几分钟|换一个模型/);
assert.doesNotMatch(label, /直接重试/);
});
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-context-compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ContextCompactionNotice {

export function contextCompactionNotice(
outcome: ContextCompactionOutcome,
uiLocale: UiLocale = 'en',
uiLocale: UiLocale,
): ContextCompactionNotice {
const copy = getShellCopy(uiLocale).app;
if (outcome.kind === 'compacted') {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/attachment-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type PreflightItem = {
* File blobs are sized by the browser File object; approval-token attachments
* are sized by the pending size stamped at pick time (main re-stats).
*/
export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale = 'zh-CN'): void {
export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale): void {
const copy = getDesktopConversationCopy(locale).attachments;
if (items.length > MAX_ATTACHMENT_COUNT) throw new Error(copy.tooMany);
const seen = new Set<string>();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/conversation-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { getShellRemainingCopy } from './locales/shell-remaining-copy.js';
* export that the user is going to paste somewhere public.
* - **user text** left untouched (the user typed it, they own it).
*/
export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale = 'zh-CN'): string {
export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale): string {
const copy = getShellRemainingCopy(locale).conversationExport;
const lines: string[] = [];
lines.push(`# ${sessionName}`);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/derive-turn-lineage-badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ export interface TurnLineageBadgeInput {
regeneratedToTurnId?: string;
/** True when the target turn id still exists in the materialized view. */
existsTurn(turnId: string): boolean;
locale?: UiLocale;
locale: UiLocale;
}

export function deriveTurnLineageBadges(input: TurnLineageBadgeInput): TurnLineageBadge[] {
const copy = getDesktopConversationCopy(input.locale ?? 'zh-CN').lineage;
const copy = getDesktopConversationCopy(input.locale).lineage;
const badges: TurnLineageBadge[] = [];

const forwardFrom = input.regeneratedFromTurnId ?? input.retriedFromTurnId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { cleanErrorMessage } from '../../application/contracts/connection-error-

export type CredentialPresenceStatus = boolean | 'loading' | 'error';

export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale = 'zh-CN'): string {
export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale): string {
const shared = getProviderSettingsCopy(locale).shared;
// Electron wraps ipcMain.handle rejections as "Error invoking remote method
// '<channel>': Error: <message>". Classify the original message, not the
Expand Down Expand Up @@ -59,7 +59,7 @@ export interface ConnectionTestTroubleshootingCopy {
export function connectionTestFailureFallback(
result: ConnectionTestResult,
copy: ConnectionTestTroubleshootingCopy,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): string {
const shared = getProviderSettingsCopy(locale).shared;
if (result.statusCode === 429) return shared.rateLimit;
Expand All @@ -77,14 +77,14 @@ export function connectionTestFailureFallback(
export function connectionTestFailureMessage(
result: ConnectionTestResult,
copy: ConnectionTestTroubleshootingCopy,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): string {
const fallback = connectionTestFailureFallback(result, copy, locale);
if (!result.errorMessage) return fallback;
return generalizedErrorMessageForLocale(new Error(result.errorMessage), fallback, locale);
}

export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale = 'zh-CN'): string | undefined {
export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale): string | undefined {
if (!message) return undefined;
const trimmed = message.trim();
if (!trimmed) return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const UNGROUPED_KEY = '__ungrouped__';
export function deriveProjectGroups(
sessions: ReadonlyArray<SessionSummary>,
projects: ReadonlyArray<ProjectRecord>,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): SessionHistoryGroup[] {
const sessionsByProject = new Map<string, SessionSummary[]>();
const canonicalProjectIds = new Map<string, string>();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/model-catalog-choices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function buildCatalogRecommendedDefaultModel(providerType: ProviderType):
export function buildCatalogDailyReviewModelOptions(
connections: readonly (LlmConnection & HostResolvedConnectionCatalog)[],
currentModelKey: string,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): Array<readonly [string, string]> {
const current = parseDailyReviewModelKey(currentModelKey);
const candidates: Array<{ key: string; label: string; safeSourceLabel: string }> = [];
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/model-connection-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function noRealConnectionReasonFromEvent(event: Extract<SessionEvent, { t
).reason;
}

export function noRealConnectionSetupDescription(reason: string | undefined, locale: UiLocale = 'zh-CN'): string {
export function noRealConnectionSetupDescription(reason: string | undefined, locale: UiLocale): string {
const copy = getDesktopConversationCopy(locale).model;
return reason && Object.hasOwn(copy.configurationReason, reason)
? copy.configurationReason[reason as ChatConfigurationReason]
Expand All @@ -55,7 +55,7 @@ export function noRealConnectionSetupDescription(reason: string | undefined, loc

export function sessionEventErrorMessage(
event: Extract<SessionEvent, { type: 'error' }>,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): string {
if (isNoRealConnectionEvent(event)) {
return noRealConnectionSetupDescription(noRealConnectionReasonFromEvent(event), locale);
Expand All @@ -69,7 +69,7 @@ export function sessionEventErrorMessage(
export function modelSetupToastCopy(
reason: string | undefined,
fallback: string,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): { title: string; description: string } {
const copy = getDesktopConversationCopy(locale).model;
if (reason === 'connection_missing') {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/session-error-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { getDesktopConversationCopy } from './locales/conversation-copy.js';
* runtime. Unknown reasons intentionally return undefined so callers can use
* their existing safe fallback instead of displaying raw provider text.
*/
export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale = 'zh-CN'): string | undefined {
export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale): string | undefined {
const copy = getDesktopConversationCopy(locale).turnError;
switch (reason?.toLowerCase()) {
case 'context_overflow':
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/session-status-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function normalizeSessionSummaryForDisplay<T extends SessionSummary>(sess
* the UI; they just fall through to the catch-all until the mapping
* is extended.
*/
export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale = 'zh-CN'): string {
export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale): string {
const copy = getDesktopConversationCopy(locale).turnError;
if (!errorClass) return copy.unknown;
const reasonDescription = describeSessionErrorReason(errorClass, locale);
Expand Down Expand Up @@ -169,7 +169,7 @@ export interface FailedTurnExecutionState {
*/
export function describeFailedTurnExecutionState(
state: FailedTurnExecutionState,
locale: UiLocale = 'zh-CN',
locale: UiLocale,
): string | undefined {
const copy = getDesktopConversationCopy(locale).turnError.executionState;
if (state.erroredToolCount > 0) return copy.erroredTool;
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/settings/bot-chat-shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const BOT_LABELS: Record<BotProvider, { support: 'runtime' | 'credentials
slack: { support: 'runtime' },
};

export function botReadinessCopyForSupport(support: 'runtime' | 'credentials' | 'planned', readiness: BotReadinessState, locale: UiLocale = 'zh-CN') {
export function botReadinessCopyForSupport(support: 'runtime' | 'credentials' | 'planned', readiness: BotReadinessState, locale: UiLocale) {
const copy = getBotSettingsCopy(locale);
if (support === 'planned') return copy.planned;
return copy.readiness[readiness] ?? copy.readiness.scaffolded;
Expand Down Expand Up @@ -98,7 +98,7 @@ export function BotBrandLogo(props: { provider: BotProvider; size?: 'compact' |
export type BotPendingActionName = 'test' | 'connect' | 'restart' | 'disconnect';
export type BotPendingAction = { provider: BotProvider; action: BotPendingActionName };

export function botStatusDetail(status: BotStatus, locale: UiLocale = 'zh-CN'): string {
export function botStatusDetail(status: BotStatus, locale: UiLocale): string {
const copy = getBotSettingsCopy(locale).status;
switch (status.reason) {
case 'disabled': return copy.disabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface ConnectionChipStatus {
* readiness, fixed to credential-only language. Matches the doc warning
* at SettingsModal `验证通过 ≠ 运行可用`.
*/
export function connectionChipStatus(connection: LlmConnection, locale: UiLocale = 'zh-CN'): ConnectionChipStatus | null {
export function connectionChipStatus(connection: LlmConnection, locale: UiLocale): ConnectionChipStatus | null {
const copy = getProviderSettingsCopy(locale).shared.connectionStatuses;
// Ahead of every other branch, including needs_reauth: a retired provider
// has no sign-in left to return to, so any repair-shaped status here would
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/settings/settings-error-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { type UiLocale } from '@maka/core/ui-locale';
import { redactSecrets } from '@maka/ui';
import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js';

export function settingsActionErrorMessage(error: unknown, locale: UiLocale = 'zh-CN'): string {
export function settingsActionErrorMessage(error: unknown, locale: UiLocale): string {
const raw = error instanceof Error
? error.message
: typeof error === 'string'
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/turn-footer-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export interface TurnFooterContext {
* / other action types stay clickable.
*/
pendingActions?: ReadonlySet<TurnFooterActionId>;
locale?: UiLocale;
locale: UiLocale;
}

/**
Expand All @@ -113,7 +113,7 @@ export interface TurnFooterContext {
*/
export function deriveTurnFooterActions(input: TurnFooterContext): TurnFooterAction[] {
const { status, hasContent, alreadyRegenerated, pendingActions, metaSummary } = input;
const copyText = getDesktopConversationCopy(input.locale ?? 'zh-CN').footer;
const copyText = getDesktopConversationCopy(input.locale).footer;
const actionLabel = copyText.labels;
const isPending = (id: TurnFooterActionId) => pendingActions?.has(id) ?? false;
const PENDING_TOOLTIP = copyText.pending;
Expand Down
20 changes: 4 additions & 16 deletions packages/core/src/relative-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function getAbsoluteFormat(uiLocale: UiLocale): Intl.DateTimeFormat {
* reading a relative label falls back to and a tooltip shows; `@maka/ui` had
* its own uncached copy of the same `Intl` options until this became public.
*/
export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh-CN'): string {
export function formatAbsoluteTimestamp(ts: number, locale: UiLocale): string {
return getAbsoluteFormat(locale).format(new Date(ts));
}

Expand All @@ -103,11 +103,7 @@ export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh-CN'):
* absolute date string. `now` is injectable so tests pin a deterministic clock;
* future timestamps (clock skew) snap to the just-now label.
*/
export function formatRelativeTimestamp(
ts: number,
now: number = Date.now(),
locale: UiLocale = 'zh-CN',
): string {
export function formatRelativeTimestamp(ts: number, now: number, locale: UiLocale): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs < JUST_NOW_MS) {
return JUST_NOW[locale];
Expand Down Expand Up @@ -156,11 +152,7 @@ function getCompactFormats(uiLocale: UiLocale): {
* Compact variant for wider list rows: relative inside the seven-day horizon,
* then a localized date-only label.
*/
export function formatCompactTimestamp(
ts: number,
now: number = Date.now(),
locale: UiLocale = 'zh-CN',
): string {
export function formatCompactTimestamp(ts: number, now: number, locale: UiLocale): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs <= RELATIVE_HORIZON_MS) return formatRelativeTimestamp(ts, now, locale);
const { sameYear, otherYear } = getCompactFormats(locale);
Expand All @@ -176,11 +168,7 @@ export function formatCompactTimestamp(
* Unit tokens stay deliberately locale-neutral so the trailing column remains
* stable across UI languages: "46min", "13h", "17d", "1mo", "1y".
*/
export function formatSidebarTimestamp(
ts: number,
now: number = Date.now(),
locale: UiLocale = 'zh-CN',
): string {
export function formatSidebarTimestamp(ts: number, now: number, locale: UiLocale): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs < JUST_NOW_MS) return JUST_NOW[locale];
const bucket = sidebarTimeBucket(diffMs);
Expand Down
Loading