Refactor application architecture and shared functionality

This commit is contained in:
Jakub Syty
2026-08-21 10:59:47 +02:00
398 changed files with 28819 additions and 4361 deletions
+199
View File
@@ -0,0 +1,199 @@
/**
* Client for the OpenChamber agent memory routes.
*
* The store is owned by the server (`packages/web/server/lib/agent-memory`).
* This module only speaks HTTP and resolves no storage paths.
*
* Every function throws on failure. An authoritative read must never resolve to
* an empty list a caller could mistake for "the agent remembers nothing" — that
* reading is exactly what would make the user think memory had been lost.
*
* A 404 is the one exception, and it means the feature is switched off rather
* than that the entry is missing: the server disables the whole surface, so
* callers translate it into `disabled` instead of an error.
*/
import { createProjectIdFromPath } from './projectId';
import { runtimeFetch } from './runtime-fetch';
export type AgentMemoryType = 'fact' | 'preference' | 'reference';
export type AgentMemoryScope = 'global' | 'project';
export interface AgentMemoryEntry {
id: string;
title: string;
body: string;
type: AgentMemoryType;
createdAt: number;
updatedAt: number;
/**
* Reads as an instruction to the model rather than a fact. Kept in the store
* and shown here, but withheld from what sessions are told.
*/
flagged?: boolean;
/** The session this was learned in, when the agent recorded one. */
sessionId?: string;
}
interface AgentMemorySnapshot {
global: AgentMemoryEntry[];
project: AgentMemoryEntry[];
/**
* A scope that failed to load. Kept separate from an empty list so the panel
* can say "could not load" rather than showing an empty tab that reads as
* "the agent has forgotten everything".
*/
globalFailed: boolean;
projectFailed: boolean;
}
/** Mirrors the server's clamps, so the editor stops where storage would cut. */
export const AGENT_MEMORY_TITLE_MAX_LENGTH = 120;
export const AGENT_MEMORY_BODY_MAX_LENGTH = 2000;
/** Raised when the server reports the whole memory surface as switched off. */
export class AgentMemoryDisabledError extends Error {
constructor() {
super('Agent memory is disabled');
this.name = 'AgentMemoryDisabledError';
}
}
const BASE_PATH = '/api/agent-memory';
/**
* Mirrors the server: the storage id comes from the project path, not from
* `project.id`, because the path-derived id is what names the file on disk.
*/
const resolveMemoryProjectId = (projectPath: string | null | undefined): string => {
const trimmed = typeof projectPath === 'string' ? projectPath.trim() : '';
return trimmed ? createProjectIdFromPath(trimmed) : '';
};
const scopeQuery = (scope: AgentMemoryScope, projectId: string): string => {
if (scope === 'global') {
return 'scope=global';
}
if (!projectId) {
throw new Error('Project memory needs a resolvable project path');
}
return `scope=project&projectId=${encodeURIComponent(projectId)}`;
};
interface ErrorPayload {
error?: unknown;
disabled?: unknown;
}
/**
* A 404 alone does not mean the feature is off — a deleted entry answers 404
* too. Only the server's explicit `disabled` flag distinguishes them.
*/
const failed = async (response: Response, fallback: string): Promise<never> => {
let payload: ErrorPayload | null = null;
try {
payload = await response.json() as ErrorPayload | null;
} catch {
// Fall through to the generic message.
}
if (response.status === 404 && payload?.disabled === true) {
throw new AgentMemoryDisabledError();
}
const message = typeof payload?.error === 'string' && payload.error.trim()
? payload.error
: `${fallback} (${response.status})`;
throw new Error(message);
};
const parseEntry = (value: unknown): AgentMemoryEntry | null => {
const record = value as Partial<AgentMemoryEntry> | null;
if (!record || typeof record !== 'object') {
return null;
}
if (typeof record.id !== 'string' || typeof record.title !== 'string' || typeof record.body !== 'string') {
return null;
}
return {
id: record.id,
title: record.title,
body: record.body,
type: record.type === 'preference' || record.type === 'reference' ? record.type : 'fact',
createdAt: typeof record.createdAt === 'number' ? record.createdAt : 0,
updatedAt: typeof record.updatedAt === 'number' ? record.updatedAt : 0,
...(record.flagged === true ? { flagged: true } : {}),
...(typeof record.sessionId === 'string' ? { sessionId: record.sessionId } : {}),
};
};
const parseEntries = (value: unknown): AgentMemoryEntry[] => (
Array.isArray(value) ? value.map(parseEntry).filter((entry): entry is AgentMemoryEntry => entry !== null) : []
);
/**
* Both scopes in one request. Two requests would let one scope render while the
* other is still in flight, which reads as memory that has gone missing.
*/
export const fetchAgentMemory = async (
projectPath: string | null,
options: { signal?: AbortSignal } = {},
): Promise<AgentMemorySnapshot> => {
const projectId = resolveMemoryProjectId(projectPath);
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : '';
const response = await runtimeFetch(`${BASE_PATH}/all${query}`, {
cache: 'no-store',
signal: options.signal,
});
if (!response.ok) {
return failed(response, 'Failed to load agent memory');
}
const payload = await response.json() as Record<string, unknown> | null;
if (!payload || typeof payload !== 'object') {
throw new Error('Malformed agent memory response');
}
return {
global: parseEntries(payload.global),
project: parseEntries(payload.project),
globalFailed: payload.globalFailed === true,
projectFailed: payload.projectFailed === true,
};
};
/** A user correction from the panel; the agent rewrites by saving again. */
export const updateAgentMemory = async (
scope: AgentMemoryScope,
projectPath: string | null,
memoryId: string,
patch: { title?: string; body?: string; type?: AgentMemoryType },
): Promise<AgentMemoryEntry> => {
const query = scopeQuery(scope, resolveMemoryProjectId(projectPath));
const response = await runtimeFetch(`${BASE_PATH}/${encodeURIComponent(memoryId)}?${query}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
if (!response.ok) {
return failed(response, 'Failed to save memory');
}
const payload = await response.json() as { entry?: unknown } | null;
const entry = parseEntry(payload?.entry);
if (!entry) {
throw new Error('Malformed agent memory response');
}
return entry;
};
export const deleteAgentMemory = async (
scope: AgentMemoryScope,
projectPath: string | null,
memoryId: string,
): Promise<void> => {
const query = scopeQuery(scope, resolveMemoryProjectId(projectPath));
const response = await runtimeFetch(`${BASE_PATH}/${encodeURIComponent(memoryId)}?${query}`, {
method: 'DELETE',
});
if (!response.ok) {
await failed(response, 'Failed to delete memory');
}
};
@@ -0,0 +1,74 @@
import { describe, expect, test } from 'bun:test';
import { classifyMemory, countHighlightedMemories, memoryViewKey } from './agentMemoryBadges';
import type { AgentMemoryEntry } from './agentMemoryApi';
const entry = (overrides: Partial<AgentMemoryEntry> = {}): AgentMemoryEntry => ({
id: 'mem-1',
title: 'Uses bun',
body: 'Tests run with bun test.',
type: 'fact',
createdAt: 100,
updatedAt: 100,
...overrides,
});
describe('classifying an entry against the last look', () => {
test('an entry stored since the last look is new', () => {
expect(classifyMemory(entry({ createdAt: 200, updatedAt: 200 }), 100)).toBe('new');
});
test('an entry rewritten since the last look is changed, not new', () => {
// The distinction matters: a memory the agent invented and one it quietly
// rewrote need different attention.
expect(classifyMemory(entry({ createdAt: 50, updatedAt: 200 }), 100)).toBe('changed');
});
test('an untouched entry carries no badge', () => {
expect(classifyMemory(entry({ createdAt: 50, updatedAt: 50 }), 100)).toBeNull();
});
test('a rewrite the user already saw carries no badge', () => {
expect(classifyMemory(entry({ createdAt: 10, updatedAt: 50 }), 100)).toBeNull();
});
test('everything is new before the user has ever looked', () => {
expect(classifyMemory(entry({ createdAt: 1, updatedAt: 1 }), 0)).toBe('new');
});
test('an entry stored exactly at the last look is not re-announced', () => {
expect(classifyMemory(entry({ createdAt: 100, updatedAt: 100 }), 100)).toBeNull();
});
});
describe('counting what deserves a glance', () => {
test('counts new and changed together', () => {
const count = countHighlightedMemories([
entry({ id: 'a', createdAt: 200, updatedAt: 200 }),
entry({ id: 'b', createdAt: 50, updatedAt: 200 }),
entry({ id: 'c', createdAt: 50, updatedAt: 50 }),
], 100);
expect(count).toBe(2);
});
test('an untouched store counts nothing', () => {
expect(countHighlightedMemories([entry({ createdAt: 1, updatedAt: 1 })], 100)).toBe(0);
});
});
describe('where each scope keeps its mark', () => {
test('global has one mark', () => {
expect(memoryViewKey('global', '/tmp/anything')).toBe('global');
});
test('each project keeps its own', () => {
// One shared project mark would let opening one project silently clear
// another project's badges.
expect(memoryViewKey('project', '/tmp/a')).not.toBe(memoryViewKey('project', '/tmp/b'));
});
test('a project scope with no path never collides with global', () => {
expect(memoryViewKey('project', null)).not.toBe('global');
});
});
+44
View File
@@ -0,0 +1,44 @@
/**
* What is new or changed in agent memory since the user last looked.
*
* Derived from the entry's own timestamps against a per-scope "last viewed"
* mark, so the store carries no review state and the user is never asked to
* confirm anything. Looking at the tab is the acknowledgement.
*
* The two badges are worth separating: a memory the agent has just invented
* and one it has quietly rewritten need different attention, and lumping them
* together as "new" would hide every correction.
*/
import type { AgentMemoryEntry, AgentMemoryScope } from './agentMemoryApi';
export type MemoryBadge = 'new' | 'changed' | null;
/**
* The key a scope's mark is stored under. Project marks are keyed by path
* because each project has its own store — one shared mark would let opening
* one project silently clear another's badges.
*/
export const memoryViewKey = (scope: AgentMemoryScope, projectPath: string | null): string => (
scope === 'global' ? 'global' : `project:${projectPath ?? ''}`
);
/**
* `viewedAt` of 0 means the user has never opened this scope. Everything stored
* is then genuinely new to them, which is what a first look should show.
*/
export const classifyMemory = (entry: AgentMemoryEntry, viewedAt: number): MemoryBadge => {
if (entry.createdAt > viewedAt) {
return 'new';
}
// Only a change the user has not seen counts. An entry rewritten before their
// last look was already accounted for by that look.
if (entry.updatedAt > viewedAt) {
return 'changed';
}
return null;
};
export const countHighlightedMemories = (entries: AgentMemoryEntry[], viewedAt: number): number => (
entries.reduce((total, entry) => (classifyMemory(entry, viewedAt) ? total + 1 : total), 0)
);
@@ -25,4 +25,9 @@ describe('FilesystemError', () => {
expect(parseFilesystemErrorReason('made-up')).toBe('unknown');
expect(parseFilesystemErrorReason(undefined)).toBe('unknown');
});
test('recognizes filesystem errors created across runtime boundaries', () => {
expect(isFilesystemError({ reason: 'already-exists' })).toBe(true);
expect(isFilesystemError({ reason: 409 })).toBe(false);
});
});
+2
View File
@@ -1,5 +1,6 @@
export type FilesystemErrorReason =
| 'os-permission'
| 'already-exists'
| 'not-found'
| 'not-directory'
| 'invalid-response'
@@ -30,6 +31,7 @@ export const isFilesystemError = (error: unknown): error is FilesystemError => (
export const parseFilesystemErrorReason = (value: unknown): FilesystemErrorReason => {
switch (value) {
case 'os-permission':
case 'already-exists':
case 'not-found':
case 'not-directory':
case 'invalid-response':
+6 -22
View File
@@ -606,6 +606,7 @@ export interface FilesAPI {
readFile?(path: string, options?: FileReadOptions): Promise<{ content: string; path: string }>;
readFileBinary?(path: string, options?: FileReadOptions): Promise<{ dataUrl: string; path: string }>;
writeFile?(path: string, content: string): Promise<{ success: boolean; path: string }>;
uploadFile?(path: string, file: Blob, options?: { overwrite?: boolean; directory?: string }): Promise<{ success: boolean; path: string }>;
delete?(path: string): Promise<{ success: boolean }>;
rename?(oldPath: string, newPath: string): Promise<{ success: boolean; path: string }>;
revealPath?(path: string): Promise<{ success: boolean }>;
@@ -1246,7 +1247,7 @@ export type RuntimeAPISelector<TValue> = (apis: RuntimeAPIs) => TValue;
type SkillsCatalogSourceId = string;
type SkillsCatalogSourceType = 'github' | 'clawdhub';
type SkillsCatalogSourceType = 'github';
export interface SkillsCatalogSource {
id: SkillsCatalogSourceId;
@@ -1255,6 +1256,10 @@ export interface SkillsCatalogSource {
source: string;
defaultSubpath?: string;
sourceType?: SkillsCatalogSourceType;
/** GitHub repository star count (null when unavailable) */
stars?: number | null;
/** GitHub repository last-push timestamp, ISO (null when unavailable) */
repoUpdatedAt?: string | null;
}
interface SkillsCatalogItemInstalledBadge {
@@ -1263,18 +1268,6 @@ interface SkillsCatalogItemInstalledBadge {
source?: 'opencode' | 'agents' | 'claude';
}
interface ClawdHubSkillMetadata {
slug: string;
version: string;
displayName?: string;
owner?: string;
downloads?: number;
stars?: number;
versionsCount?: number;
createdAt?: number;
updatedAt?: number;
}
export interface SkillsCatalogItem {
sourceId: SkillsCatalogSourceId;
repoSource: string;
@@ -1287,22 +1280,18 @@ export interface SkillsCatalogItem {
installable: boolean;
warnings?: string[];
installed?: SkillsCatalogItemInstalledBadge;
/** ClawdHub-specific metadata (present only for ClawdHub sources) */
clawdhub?: ClawdHubSkillMetadata;
}
export interface SkillsCatalogResponse {
ok: boolean;
sources?: SkillsCatalogSource[];
itemsBySource?: Record<SkillsCatalogSourceId, SkillsCatalogItem[]>;
pageInfoBySource?: Record<SkillsCatalogSourceId, { nextCursor?: string | null }>;
error?: { kind: string; message: string };
}
export interface SkillsCatalogSourceResponse {
ok: boolean;
items?: SkillsCatalogItem[];
nextCursor?: string | null;
error?: { kind: string; message: string };
}
@@ -1327,11 +1316,6 @@ export interface SkillsRepoScanResponse {
interface SkillsInstallSelection {
skillDir: string;
/** ClawdHub-specific metadata for installation */
clawdhub?: {
slug: string;
version: string;
};
}
export interface SkillsInstallRequest {
@@ -61,6 +61,12 @@ describe('annotation overlay script', () => {
expect(script).toContain(JSON.stringify(labels.commentPlaceholder));
});
test('keeps comment keystrokes away from shortcuts on the annotated page', () => {
expect(script).toContain("comment.addEventListener('keydown', onCommentKeyDown)");
expect(script).toContain('var onCommentKeyDown = function (event) {');
expect(script).toContain('event.stopPropagation();');
});
test('escapes a label that would otherwise close the script', () => {
const hostile = buildAnnotationOverlayScript(theme, {
...labels,
@@ -534,9 +534,13 @@ export const buildAnnotationOverlayScript = (
event.preventDefault();
event.stopImmediatePropagation();
finish(null);
return;
}
if (event.key === 'Enter' && !event.shiftKey && event.target === comment) {
};
var onCommentKeyDown = function (event) {
// Do not let the annotated page treat typed letters as its own shortcuts.
event.stopPropagation();
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
attach();
}
@@ -551,6 +555,7 @@ export const buildAnnotationOverlayScript = (
window.addEventListener('scroll', onScrollOrResize, true);
window.addEventListener('resize', onScrollOrResize, true);
window.addEventListener('keydown', onKeyDown, true);
comment.addEventListener('keydown', onCommentKeyDown);
// ------------------------------------------------------------------ finish
@@ -564,6 +569,7 @@ export const buildAnnotationOverlayScript = (
window.removeEventListener('scroll', onScrollOrResize, true);
window.removeEventListener('resize', onScrollOrResize, true);
window.removeEventListener('keydown', onKeyDown, true);
comment.removeEventListener('keydown', onCommentKeyDown);
setCursor('');
if (cursorStyle.parentNode) cursorStyle.parentNode.removeChild(cursorStyle);
if (host.parentNode) host.parentNode.removeChild(host);
+2
View File
@@ -155,6 +155,8 @@ export type DesktopSettings = {
showOpenCodeUpdateNotifications?: boolean;
agentControlToolEnabled?: boolean;
agentWebToolEnabled?: boolean;
agentMemoryToolEnabled?: boolean;
agentMemoryFeatureAvailable?: boolean;
optimizeSystemPrompt?: boolean;
openCodeUpdateToastDismissedVersion?: string;
showToolFileIcons?: boolean;
@@ -0,0 +1,38 @@
import { describe, expect, test } from 'bun:test';
import {
notifyFileContentInvalidated,
subscribeToFileContentInvalidation,
} from './fileContentInvalidation';
describe('fileContentInvalidation', () => {
test('publishes normalized paths within the captured runtime', () => {
const received: Array<{ runtimeKey: string; paths: readonly string[] }> = [];
const unsubscribe = subscribeToFileContentInvalidation((invalidation) => {
received.push(invalidation);
});
notifyFileContentInvalidated({
runtimeKey: ' runtime-a ',
paths: [' /repo/a.txt ', '/repo/a.txt', '', '/repo/b.txt'],
});
unsubscribe();
expect(received).toEqual([{
runtimeKey: 'runtime-a',
paths: ['/repo/a.txt', '/repo/b.txt'],
}]);
});
test('stops publishing after unsubscribe', () => {
let calls = 0;
const unsubscribe = subscribeToFileContentInvalidation(() => {
calls += 1;
});
unsubscribe();
notifyFileContentInvalidated({ runtimeKey: 'runtime-a', paths: ['/repo/a.txt'] });
expect(calls).toBe(0);
});
});
@@ -0,0 +1,25 @@
type FileContentInvalidation = {
runtimeKey: string;
paths: readonly string[];
};
type FileContentInvalidationListener = (invalidation: FileContentInvalidation) => void;
const listeners = new Set<FileContentInvalidationListener>();
export const notifyFileContentInvalidated = (invalidation: FileContentInvalidation): void => {
const runtimeKey = invalidation.runtimeKey.trim();
const paths = Array.from(new Set(invalidation.paths.map((path) => path.trim()).filter(Boolean)));
if (!runtimeKey || paths.length === 0) return;
for (const listener of listeners) {
listener({ runtimeKey, paths });
}
};
export const subscribeToFileContentInvalidation = (
listener: FileContentInvalidationListener,
): (() => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
+5 -5
View File
@@ -2,7 +2,7 @@
import * as gitHttp from './gitApiHttp';
import { opencodeClient } from './opencode/client';
import { renderMagicPrompt } from './magicPrompts';
import { runtimeFetch } from './runtime-fetch';
import { requestSmallModel } from './smallModelRequest';
import { materializeOpenDraftSession, useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -283,7 +283,7 @@ export async function generateCommitMessage(
try {
const diffs = await collectSelectedFileDiffs(directory, files);
const { currentProviderId, currentModelId } = useConfigStore.getState();
const response = await runtimeFetch('/api/small-model/generate', {
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -293,7 +293,7 @@ export async function generateCommitMessage(
...(currentProviderId ? { preferredProviderID: currentProviderId } : {}),
...(currentModelId ? { preferredModelID: currentModelId } : {}),
}),
});
}, { silentStatuses: [404] });
if (response.status === 404) {
// No authenticated provider has a small model — fall back to the
@@ -411,7 +411,7 @@ export async function generatePullRequestDescription(
try {
const { currentProviderId, currentModelId } = useConfigStore.getState();
const response = await runtimeFetch('/api/small-model/generate', {
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -421,7 +421,7 @@ export async function generatePullRequestDescription(
...(currentProviderId ? { preferredProviderID: currentProviderId } : {}),
...(currentModelId ? { preferredModelID: currentModelId } : {}),
}),
});
}, { silentStatuses: [404] });
if (response.status === 404) {
// No authenticated provider has a small model — fall back to the
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go Nutzungsverfolgung',
'settings.providers.page.openCodeGo.description': 'Verbinden Sie das OpenCode Go Dashboard, um rollierenden, wöchentlichen und monatlichen Verbrauch anzuzeigen.',
@@ -844,16 +845,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': 'Manuell',
'settings.skills.catalog.page.mode.external': 'Extern',
'settings.skills.catalog.page.title': 'Fähigkeitskatalog',
'settings.skills.catalog.page.subtitle': 'Installiere fertige Skills aus kuratierten Repositories oder füge eine eigene Quelle hinzu.',
'settings.skills.catalog.page.section.sources': 'Quellen',
'settings.skills.catalog.page.searchAllPlaceholder': 'Skills in allen Quellen suchen…',
'settings.skills.catalog.page.search.clear': 'Suche löschen',
'settings.skills.catalog.page.source.skillsCount': 'Skills: {count}',
'settings.skills.catalog.page.source.stars': 'Sterne: {count}',
'settings.skills.catalog.page.source.updated': 'Aktualisiert {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Eigene Quelle hinzufügen',
'settings.skills.catalog.page.source.addOwnDescription': 'Beliebiges Git-Repository mit Skills',
'settings.skills.catalog.page.source.viewRepo': 'Repository auf GitHub öffnen',
'settings.skills.catalog.page.skill.viewOnGithub': 'Skill auf GitHub ansehen',
'settings.skills.catalog.page.list.searchTitle': 'Suchergebnisse',
'settings.skills.catalog.page.section.sourceRepository': 'Quell-Repository',
'settings.skills.catalog.page.field.selectSourcePlaceholder': 'Quelle auswählen',
'settings.skills.catalog.page.actions.refreshTitle': 'Aktualisieren',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'Katalog entfernen',
'settings.skills.catalog.page.actions.addCatalog': 'Katalog hinzufügen',
'settings.skills.catalog.page.actions.removeCatalog': 'Katalog entfernen',
'settings.skills.catalog.page.actions.loadMoreSkills': 'Weitere Fähigkeiten laden',
'settings.skills.catalog.page.loading.catalog': 'Wird geladen...',
'settings.skills.catalog.page.loading.skills': 'Fähigkeiten werden geladen...',
'settings.skills.catalog.page.loading.more': 'Wird geladen...',
'settings.skills.catalog.page.foundCount': '{count} Fähigkeit(en) gefunden',
'settings.skills.catalog.page.error.catalogTitle': 'Katalogfehler',
'settings.skills.catalog.page.empty.noSkillsTitle': 'Keine Fähigkeiten gefunden',
@@ -861,7 +872,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': 'installiert ({scope})',
'settings.skills.catalog.page.badge.notInstallable': 'nicht installierbar',
'settings.skills.catalog.page.badge.unknown': 'unbekannt',
'settings.skills.catalog.page.byOwnerPrefix': 'von',
'settings.skills.catalog.page.removeDialog.title': 'Katalog entfernen',
'settings.skills.catalog.page.removeDialog.description': 'Sind Sie sicher, dass Sie diesen Katalog entfernen möchten?',
'settings.openchamber.passkeys.title': 'Passkeys',
@@ -950,6 +960,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber-Web-Werkzeug',
'settings.openchamber.tools.field.agentWebToolAria': 'Das OpenChamber-Web-Werkzeug aktivieren',
'settings.openchamber.tools.field.agentWebToolInfo': 'Lässt Agenten die Seite im Browser-Panel von OpenChamber ansehen und bedienen: eine URL öffnen, den Inhalt lesen, klicken, tippen, scrollen und zwischen mobiler und Desktop-Ansicht wechseln. Fügt jeder Sitzung eine kleine Werkzeugbeschreibung hinzu. Gilt nach einem Neustart von OpenCode.',
'settings.openchamber.tools.field.agentMemoryTool': 'Agenten-Gedächtniswerkzeug',
'settings.openchamber.tools.field.agentMemoryToolAria': 'Agenten-Gedächtniswerkzeug',
'settings.openchamber.tools.field.agentMemoryToolInfo': 'Lässt Agenten Gelerntes über Sitzungen hinweg behalten, in zwei Speichern: was über Sie zutrifft und was über das jeweilige Projekt zutrifft. Sitzungen erhalten die gespeicherten Titel, damit der Agent bei Bedarf einen Eintrag lesen kann. Beim Ausschalten entfallen Werkzeug, Gedächtnis-Tab und Sitzungsindex. Gilt nach einem Neustart von OpenCode.',
'settings.openchamber.opencodeCli.tooltipPrefix': 'Optionaler absoluter Pfad zur',
'settings.openchamber.opencodeCli.tooltipSuffix': 'Binary.',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode Binary-Pfad',
@@ -1130,6 +1143,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': '{provider} Nutzung',
'settings.usage.page.header.refreshing': 'Aktualisiere Nutzung...',
'settings.usage.page.header.lastUpdated': 'Zuletzt aktualisiert: {time}',
'settings.usage.page.header.lastUpdatedWithPlan': 'Tarif: {plan} · Zuletzt aktualisiert: {time}',
'settings.usage.page.options.showInWorkStatusAria': 'Im Arbeitsstatusbereich anzeigen',
'settings.usage.page.options.showInWorkStatus': 'Im Arbeitsstatusbereich anzeigen',
'settings.usage.page.options.showInWorkStatusTooltip': 'Wenn aktiviert, ist die Nutzung dieses Anbieters im Arbeitsstatusbereich sichtbar.',
@@ -1295,13 +1309,18 @@ export const settingsDict = {
'settings.providers.page.custom.optionLabel': 'Andere / Benutzerdefiniert',
'settings.providers.page.custom.title': 'Benutzerdefinierter Anbieter',
'settings.providers.page.custom.editTitle': 'Benutzerdefinierten Anbieter bearbeiten',
'settings.providers.page.custom.description': 'Fügen Sie einen OpenAI-kompatiblen Anbieter mit Basis-URL, Anmeldedaten und Modellliste hinzu. Wird in der OpenCode-Konfiguration gespeichert und steht im Chat wie jeder andere Anbieter zur Verfügung.',
'settings.providers.page.custom.description': 'Fügen Sie einen Anbieter mit Basis-URL, Anmeldedaten, Modellliste und unterstütztem API-Protokoll hinzu. Wird zur Nutzung im Chat in der OpenCode-Konfiguration gespeichert.',
'settings.providers.page.custom.field.providerID.label': 'Anbieter-ID',
'settings.providers.page.custom.field.providerID.placeholder': 'mein-anbieter',
'settings.providers.page.custom.field.providerID.info': 'Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche. Wird als OpenCode-Anbieter-ID verwendet.',
'settings.providers.page.custom.field.name.label': 'Anzeigename',
'settings.providers.page.custom.field.name.placeholder': 'Mein Anbieter',
'settings.providers.page.custom.field.name.info': 'Wird in den Anbieter- und Modellauswahlen angezeigt.',
'settings.providers.page.custom.field.protocol.label': 'API-Protokoll',
'settings.providers.page.custom.field.protocol.info': 'Wählen Sie das Anfrageformat, das diese API implementiert.',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': 'Basis-URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'OpenAI-kompatible API-Basis-URL. Muss mit http:// oder https:// beginnen.',
@@ -2124,4 +2143,5 @@ export const settingsDict = {
'settings.openchamber.visual.option.themeMode.light.description': 'Immer helles Erscheinungsbild verwenden',
'settings.openchamber.visual.option.themeMode.dark.description': 'Immer dunkles Erscheinungsbild verwenden',
'chat.message.userText.collapseAria': 'Benutzernachricht einklappen',
...thirdPartyIntegrationI18n.de,
};
+70 -14
View File
@@ -933,6 +933,8 @@ export const dict = {
'gitView.pr.field.draft': 'Entwurf',
'gitView.pr.field.title': 'Titel',
'gitView.pr.githubNotConnected': 'GitHub ist nicht verbunden',
'gitView.pr.history.merged': 'PR #{number} wurde in {base} gemergt.',
'gitView.pr.history.closed': 'PR #{number} wurde geschlossen.',
'gitView.pr.loadingDescription': 'Beschreibung wird geladen...',
'gitView.pr.mergeMethod.merge': 'Einen Merge-Commit erstellen',
'gitView.pr.mergeMethod.rebase': 'Rebase und Merge',
@@ -1170,6 +1172,14 @@ export const dict = {
'sidebarFilesTree.toast.writeNotSupported': 'Schreiben nicht unterstützt',
'sidebarFilesTree.toast.fileCreated': 'Datei erstellt',
'sidebarFilesTree.toast.operationFailed': 'Operation fehlgeschlagen',
'sidebarFilesTree.toast.uploaded': 'Dateien hochgeladen',
'sidebarFilesTree.toast.uploadedWithoutConflicts': 'Dateien ohne Konflikte wurden hochgeladen',
'sidebarFilesTree.toast.uploadFailed': 'Einige Dateien konnten nicht hochgeladen werden',
'sidebarFilesTree.drop.target': 'In {path} hochladen',
'sidebarFilesTree.drop.uploading': 'Dateien werden in {path} hochgeladen',
'sidebarFilesTree.dialog.uploadConflicts.title': 'Vorhandene Dateien ersetzen?',
'sidebarFilesTree.dialog.uploadConflicts.description': 'Dateien mit diesen Namen sind in {path} bereits vorhanden. Das Ersetzen kann nicht rückgängig gemacht werden.',
'sidebarFilesTree.dialog.uploadConflicts.replace': 'Ersetzen',
'sidebarFilesTree.toast.folderNameRequired': 'Ordnername ist erforderlich',
'sidebarFilesTree.toast.folderCreated': 'Ordner erstellt',
'sidebarFilesTree.toast.nameRequired': 'Name ist erforderlich',
@@ -1274,10 +1284,12 @@ export const dict = {
'contextUsage.mobile.usedTokens': 'Verwendete Tokens',
'contextUsage.mobile.contextLimit': 'Kontextlimit',
'contextUsage.mobile.outputLimit': 'Ausgabelimit',
'contextUsage.mobile.cost': 'Kosten',
'contextUsage.mobile.usage': 'Nutzung',
'contextUsage.tooltip.usedTokens': 'Verwendete Tokens: {tokens}',
'contextUsage.tooltip.contextLimit': 'Kontextlimit: {tokens}',
'contextUsage.tooltip.outputLimit': 'Ausgabelimit: {tokens}',
'contextUsage.tooltip.cost': 'Kosten: {cost}',
'contextSidebar.session.untitled': 'Unbenannte Sitzung',
'contextSidebar.empty.openSession': 'Öffnen Sie eine Sitzung, um den Kontext zu prüfen.',
'contextSidebar.section.context': 'Kontext',
@@ -1304,6 +1316,7 @@ export const dict = {
'planView.file.defaultName': 'plan',
'planView.title.default': 'Plan',
'planView.error.saveFailed': 'Speichern fehlgeschlagen',
'planView.error.loadFailed': 'Plan konnte nicht geladen werden',
'planView.error.previewUnavailable': 'Vorschau nicht verfügbar',
'planView.error.switchToEditMode': 'Wechseln Sie zum Bearbeitungsmodus, um das Problem zu beheben.',
'planView.error.writeFailed': 'Schreiben fehlgeschlagen',
@@ -1386,11 +1399,46 @@ export const dict = {
'diffView.hunk.unsupported': 'Das Staging einzelner Stücke wird in dieser Laufzeitumgebung nicht unterstützt.',
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Wähle ein Projekt aus, um Notizen und Aufgaben hinzuzufügen.',
'rightSidebar.contextNotesTodo.notes.title': 'Schnelle Notizen - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': 'Kontext, Erinnerungen oder Links festhalten',
'rightSidebar.contextNotesTodo.todo.title': 'Aufgaben',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count} Element',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count} Elemente',
'rightSidebar.contextNotesTodo.notes.addAria': 'Notiz hinzufügen',
'rightSidebar.contextNotesTodo.notes.empty': 'Noch keine Notizen. Halte Kontext, Erinnerungen oder Links fest.',
'rightSidebar.contextNotesTodo.notes.actions.expand': 'Notiz aufklappen',
'rightSidebar.contextNotesTodo.notes.actions.collapse': 'Notiz zuklappen',
'rightSidebar.contextNotesTodo.notes.actions.delete': 'Notiz löschen',
'rightSidebar.contextNotesTodo.notes.actions.pin': 'An Agent-Kontext anheften',
'rightSidebar.contextNotesTodo.notes.actions.unpin': 'Vom Agent-Kontext lösen',
'rightSidebar.contextNotesTodo.notes.source.selection': 'Aus dem Chat',
'rightSidebar.contextNotesTodo.notes.source.agent': 'Vom Agenten',
'rightSidebar.contextNotesTodo.search.placeholder': 'Suchen',
'rightSidebar.contextNotesTodo.search.clear': 'Suche zurücksetzen',
'rightSidebar.contextNotesTodo.search.noResults': 'Nichts passt zu "{query}".',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': 'Notiz konnte nicht gelöscht werden',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': 'Notiz konnte nicht erstellt werden',
'rightSidebar.contextNotesTodo.tabs.notes': 'Notizen',
'rightSidebar.contextNotesTodo.tabs.todos': 'Todo',
'rightSidebar.contextNotesTodo.tabs.plans': 'Pläne',
'rightSidebar.contextNotesTodo.plans.actions.back': 'Zurück zu den Plänen',
'rightSidebar.contextNotesTodo.tabs.memory': 'Gedächtnis',
'rightSidebar.contextNotesTodo.sections.label': 'Bereiche des Projektkontexts',
'rightSidebar.contextNotesTodo.sections.resize': 'Breite der Bereichsleiste ändern',
'rightSidebar.contextNotesTodo.memory.scope.project': 'Projekt',
'rightSidebar.contextNotesTodo.memory.scope.label': 'Gedächtnisbereich',
'rightSidebar.contextNotesTodo.memory.scope.global': 'Über Sie',
'rightSidebar.contextNotesTodo.memory.type.fact': 'Fakt',
'rightSidebar.contextNotesTodo.memory.badge.new': 'neu',
'rightSidebar.contextNotesTodo.memory.flagged': 'Vom Agenten zurückgehalten — liest sich wie eine Anweisung',
'rightSidebar.contextNotesTodo.memory.badge.changed': 'geändert',
'rightSidebar.contextNotesTodo.memory.type.preference': 'Präferenz',
'rightSidebar.contextNotesTodo.memory.type.reference': 'Verweis',
'rightSidebar.contextNotesTodo.memory.actions.delete': 'Diesen Eintrag vergessen',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': 'Titel des Eintrags',
'rightSidebar.contextNotesTodo.memory.actions.editBody': 'Text des Eintrags',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': 'Eintrag konnte nicht gespeichert werden',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': 'Eintrag konnte nicht vergessen werden',
'rightSidebar.contextNotesTodo.memory.empty.nothing': 'Der Agent hat hier noch nichts gespeichert.',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': 'Kein gespeicherter Eintrag passt zur Suche.',
'rightSidebar.contextNotesTodo.memory.empty.noProject': 'Öffnen Sie ein Projekt, um zu sehen, woran sich der Agent erinnert.',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': 'Gespeichertes Gedächtnis konnte nicht geladen werden. Es ging nichts verloren — bitte erneut versuchen.',
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Abgeschlossene löschen',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'Eine Aufgabe hinzufügen',
'rightSidebar.contextNotesTodo.todo.addAria': 'Aufgabe hinzufügen',
@@ -1401,13 +1449,9 @@ export const dict = {
'rightSidebar.contextNotesTodo.todo.actions.delete': 'Lösche "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.send': 'Sende "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Ordne "{text}" neu',
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Größe der Aufgabenliste ändern',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'An aktuelle Sitzung senden',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'An neue Sitzung senden',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'An neue Worktree-Sitzung senden',
'rightSidebar.contextNotesTodo.plans.title': 'Pläne',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} Datei',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} Dateien',
'rightSidebar.contextNotesTodo.plans.importFromFile': 'Plan aus Datei importieren',
'rightSidebar.contextNotesTodo.plans.empty': 'Noch keine gespeicherten Pläne.',
'rightSidebar.contextNotesTodo.plans.deletePlan': 'Plan löschen',
@@ -1427,6 +1471,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': 'Todo an neue Sitzung gesendet',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': 'Todo an neue Worktree-Sitzung gesendet',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': 'Fehler beim Senden des Todos',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': 'Plan konnte nicht aktualisiert werden',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': 'Fehler beim Löschen des Plans',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': 'Plan-Datei ist leer',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': 'Fehler beim Importieren des Plans',
@@ -1856,6 +1901,9 @@ export const dict = {
'chat.revert.toast.undo': 'Zurückgesetzt auf {preview}',
'chat.revert.toast.redo': 'Wiederholt',
'chat.revert.toast.restored': 'Alle Nachrichten wiederhergestellt',
'chat.toast.opencodeRestartInterrupted.title': 'Chat unterbrochen',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode wurde neu gestartet, während noch eine Antwort lief. Senden Sie eine Nachricht, um fortzufahren.',
'chat.toast.opencodeRestartInterrupted.openSession': 'Sitzung öffnen',
'chat.errorBoundary.title': 'Chat-Fehler',
'chat.errorBoundary.description': 'Die Chat-Oberfläche hat einen Fehler festgestellt. Dies könnte auf ein vorübergehendes Netzwerkproblem oder beschädigte Nachrichtendaten zurückzuführen sein.',
'chat.errorBoundary.sessionLabel': 'Sitzung',
@@ -2804,13 +2852,15 @@ export const dict = {
'contextFileOpen.failure.unreadable': 'Fehler beim Öffnen der Datei',
'quota.window.5h': '5-Stunden-Limit',
'quota.window.7d': '7-Tage-Limit',
'quota.window.7dSonnet': '7-Tage Sonnet-Limit',
'quota.window.7dOpus': '7-Tage Opus-Limit',
'quota.window.weekly': 'Wöchentliches Limit',
'quota.window.extraUsage': 'Zusätzliche Nutzung',
'quota.window.weekly': 'Wöchentlich',
'quota.window.daily': 'Täglich',
'quota.window.monthly': 'Monatliches Limit',
'quota.window.monthly': 'Monatlich',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Kreditguthaben',
'quota.window.monthlyCredits': 'Monatliche Credits',
'quota.window.purchasedCredits': 'Gekaufte Credits',
'quota.window.freeCredits': 'Kostenlose Credits',
'quota.window.billingCycle': 'Abrechnungszyklus',
'quota.window.auto': 'Automatisch',
'quota.window.api': 'API',
@@ -2962,12 +3012,12 @@ export const dict = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Das kleine Modell unterstützt die strukturierten Antworten nicht, die ein Walkthrough benötigt.',
'contextRail.surface.plan.description': 'Plankontext',
'contextRail.surface.pr.description': 'PR-Kontext',
'contextRail.surface.notes.description': 'Notizkontext',
'contextRail.surface.notes.description': 'Notizen, To-dos, Pläne und Agenten-Gedächtnis für das Projekt',
'contextRail.surface.context.description': 'Allgemeiner Kontext',
'contextRail.surface.browser.description': 'Browserkontext',
'contextRail.surface.preview.description': 'Vorschaukontext',
'contextRail.surface.chat.description': 'Chatkontext',
'contextRail.surface.notes': 'Notizen',
'contextRail.surface.notes': 'Projektwissen',
'contextRail.editorTree.toggle': 'Editorbaum umschalten',
'sidebarFilesTree.actions.collapseAllTitle': 'Alle einklappen',
'filesView.editor.cannotPreviewBinary': 'Binärdatei kann nicht in der Vorschau angezeigt werden',
@@ -3029,6 +3079,12 @@ export const dict = {
'chat.workStatus.subagent.askedQuestion': 'hat gefragt',
'chat.workStatus.section.contextBreakdown': 'Kontextquellen',
'chat.workStatus.breakdown.skills': 'Skills',
'chat.workStatus.breakdown.pinnedNote': 'Notiz',
'chat.workStatus.breakdown.unpin': 'Vom Kontext lösen',
'chat.workStatus.breakdown.pinnedPlan': 'Plan',
'chat.workStatus.breakdown.memory': 'Agenten-Gedächtnis',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} angeheftet',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} angeheftet',
'chat.workStatus.breakdown.mcp': 'MCP-Server',
'chat.workStatus.action.openChanges': 'Änderungen öffnen',
'chat.workStatus.action.openGit': 'Git-Panel öffnen',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go usage tracking',
'settings.providers.page.openCodeGo.description': 'Connect the OpenCode Go dashboard to show rolling, weekly, and monthly quota.',
@@ -896,16 +897,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': 'Manual',
'settings.skills.catalog.page.mode.external': 'External',
'settings.skills.catalog.page.title': 'Skills Catalog',
'settings.skills.catalog.page.subtitle': 'Install ready-made skills from curated repositories, or add your own source.',
'settings.skills.catalog.page.section.sources': 'Sources',
'settings.skills.catalog.page.searchAllPlaceholder': 'Search skills across all sources…',
'settings.skills.catalog.page.search.clear': 'Clear search',
'settings.skills.catalog.page.source.skillsCount': '{count} skills',
'settings.skills.catalog.page.source.stars': '{count} stars',
'settings.skills.catalog.page.source.updated': 'Updated {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Add your own source',
'settings.skills.catalog.page.source.addOwnDescription': 'Any Git repository with skills',
'settings.skills.catalog.page.source.viewRepo': 'Open repository on GitHub',
'settings.skills.catalog.page.skill.viewOnGithub': 'View skill on GitHub',
'settings.skills.catalog.page.list.searchTitle': 'Search results',
'settings.skills.catalog.page.section.sourceRepository': 'Source Repository',
'settings.skills.catalog.page.field.selectSourcePlaceholder': 'Select source',
'settings.skills.catalog.page.actions.refreshTitle': 'Refresh',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'Remove Catalog',
'settings.skills.catalog.page.actions.addCatalog': 'Add Catalog',
'settings.skills.catalog.page.actions.removeCatalog': 'Remove Catalog',
'settings.skills.catalog.page.actions.loadMoreSkills': 'Load More Skills',
'settings.skills.catalog.page.loading.catalog': 'Loading...',
'settings.skills.catalog.page.loading.skills': 'Loading skills...',
'settings.skills.catalog.page.loading.more': 'Loading...',
'settings.skills.catalog.page.foundCount': '{count} skill(s) found',
'settings.skills.catalog.page.error.catalogTitle': 'Catalog error',
'settings.skills.catalog.page.empty.noSkillsTitle': 'No skills found',
@@ -913,7 +924,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': 'installed ({scope})',
'settings.skills.catalog.page.badge.notInstallable': 'not installable',
'settings.skills.catalog.page.badge.unknown': 'unknown',
'settings.skills.catalog.page.byOwnerPrefix': 'by',
'settings.skills.catalog.page.removeDialog.title': 'Remove Catalog',
'settings.skills.catalog.page.removeDialog.description': 'Are you sure you want to remove this catalog?',
'settings.openchamber.passkeys.title': 'Passkeys',
@@ -1012,6 +1022,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber Web tool',
'settings.openchamber.tools.field.agentWebToolAria': 'Enable the OpenChamber Web tool',
'settings.openchamber.tools.field.agentWebToolInfo': 'Let agents look at and interact with the page in OpenChamber\'s browser panel: open a URL, read the page, click, type, scroll, and switch between mobile and desktop layouts. Adds a small tool description to each session. Applies after OpenCode restarts.',
'settings.openchamber.tools.field.agentMemoryTool': 'Agent memory tool',
'settings.openchamber.tools.field.agentMemoryToolAria': 'Agent memory tool',
'settings.openchamber.tools.field.agentMemoryToolInfo': 'Let agents keep what they learn across sessions, in two stores: what is true about you, and what is true about each project. Sessions are given the stored titles so the agent can read an entry when it is relevant. Turning this off removes the tool, the Memory tab, and the session index. Applies after OpenCode restarts.',
'settings.openchamber.opencodeCli.tooltipPrefix': 'Optional absolute path to the',
'settings.openchamber.opencodeCli.tooltipSuffix': 'binary.',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode Binary Path',
@@ -1192,6 +1205,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': '{provider} Usage',
'settings.usage.page.header.refreshing': 'Refreshing usage...',
'settings.usage.page.header.lastUpdated': 'Last updated: {time}',
'settings.usage.page.header.lastUpdatedWithPlan': 'Plan: {plan} · Last updated: {time}',
'settings.usage.page.options.showInWorkStatusAria': 'Show in work status panel',
'settings.usage.page.options.showInWorkStatus': 'Show in Work Status Panel',
'settings.usage.page.options.showInWorkStatusTooltip': 'When enabled, this provider\'s usage will be visible in the work status panel.',
@@ -1357,13 +1371,18 @@ export const settingsDict = {
'settings.providers.page.custom.optionLabel': 'Other / Custom',
'settings.providers.page.custom.title': 'Custom provider',
'settings.providers.page.custom.editTitle': 'Edit custom provider',
'settings.providers.page.custom.description': 'Add an OpenAI-compatible provider with a base URL, credentials, and model list. Saved to OpenCode config so it works in chat like any other provider.',
'settings.providers.page.custom.description': 'Add a provider with a base URL, credentials, model list, and supported API protocol. Saved to OpenCode config for use in chat.',
'settings.providers.page.custom.field.providerID.label': 'Provider ID',
'settings.providers.page.custom.field.providerID.placeholder': 'my-provider',
'settings.providers.page.custom.field.providerID.info': 'Lowercase letters, numbers, hyphens, and underscores. Used as the OpenCode provider id.',
'settings.providers.page.custom.field.name.label': 'Display name',
'settings.providers.page.custom.field.name.placeholder': 'My Provider',
'settings.providers.page.custom.field.name.info': 'Shown in the provider and model pickers.',
'settings.providers.page.custom.field.protocol.label': 'API protocol',
'settings.providers.page.custom.field.protocol.info': 'Choose the request format implemented by this API.',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': 'Base URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'OpenAI-compatible API base URL. Must start with http:// or https://.',
@@ -2123,4 +2142,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n.en,
} as const;
+70 -14
View File
@@ -997,6 +997,8 @@ export const dict = {
'gitView.pr.field.draft': 'Draft',
'gitView.pr.field.title': 'Title',
'gitView.pr.githubNotConnected': 'GitHub is not connected',
'gitView.pr.history.merged': 'PR #{number} was merged into {base}.',
'gitView.pr.history.closed': 'PR #{number} was closed.',
'gitView.pr.loadingDescription': 'Loading description...',
'gitView.pr.mergeMethod.merge': 'Create a merge commit',
'gitView.pr.mergeMethod.rebase': 'Rebase and merge',
@@ -1186,12 +1188,12 @@ export const dict = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'The small model does not support the structured responses a walkthrough needs.',
'contextRail.surface.plan.description': 'View the current plan',
'contextRail.surface.pr.description': 'Create, review, and merge the pull request for the current branch',
'contextRail.surface.notes.description': 'Notes, todos, and plans for the project',
'contextRail.surface.notes.description': 'Notes, todos, plans, and agent memory for the project',
'contextRail.surface.context.description': 'Session context and token usage',
'contextRail.surface.browser.description': 'Built-in web browser',
'contextRail.surface.preview.description': 'Dev server preview',
'contextRail.surface.chat.description': 'Session opened side by side',
'contextRail.surface.notes': 'Project notes',
'contextRail.surface.notes': 'Project knowledge',
'contextRail.editorTree.toggle': 'Toggle file tree',
'contextPanel.browser.open': 'Open browser panel',
'contextPanel.browser.addressAria': 'Browser address',
@@ -1320,6 +1322,14 @@ export const dict = {
'sidebarFilesTree.toast.writeNotSupported': 'Write not supported',
'sidebarFilesTree.toast.fileCreated': 'File created',
'sidebarFilesTree.toast.operationFailed': 'Operation failed',
'sidebarFilesTree.toast.uploaded': 'Files uploaded',
'sidebarFilesTree.toast.uploadedWithoutConflicts': 'Files without conflicts were uploaded',
'sidebarFilesTree.toast.uploadFailed': 'Some files could not be uploaded',
'sidebarFilesTree.drop.target': 'Upload to {path}',
'sidebarFilesTree.drop.uploading': 'Uploading files to {path}',
'sidebarFilesTree.dialog.uploadConflicts.title': 'Replace existing files?',
'sidebarFilesTree.dialog.uploadConflicts.description': 'Files with these names already exist in {path}. Replacing them cannot be undone.',
'sidebarFilesTree.dialog.uploadConflicts.replace': 'Replace',
'sidebarFilesTree.toast.folderNameRequired': 'Folder name is required',
'sidebarFilesTree.toast.folderCreated': 'Folder created',
'sidebarFilesTree.toast.nameRequired': 'Name is required',
@@ -1427,10 +1437,12 @@ export const dict = {
'contextUsage.mobile.usedTokens': 'Used tokens',
'contextUsage.mobile.contextLimit': 'Context limit',
'contextUsage.mobile.outputLimit': 'Output limit',
'contextUsage.mobile.cost': 'Cost',
'contextUsage.mobile.usage': 'Usage',
'contextUsage.tooltip.usedTokens': 'Used tokens: {tokens}',
'contextUsage.tooltip.contextLimit': 'Context limit: {tokens}',
'contextUsage.tooltip.outputLimit': 'Output limit: {tokens}',
'contextUsage.tooltip.cost': 'Cost: {cost}',
'contextSidebar.session.untitled': 'Untitled Session',
'contextSidebar.empty.openSession': 'Open a session to inspect context.',
'contextSidebar.section.context': 'Context',
@@ -1457,6 +1469,7 @@ export const dict = {
'planView.file.defaultName': 'plan',
'planView.title.default': 'Plan',
'planView.error.saveFailed': 'Save failed',
'planView.error.loadFailed': 'Could not load this plan',
'planView.error.previewUnavailable': 'Preview unavailable',
'planView.error.switchToEditMode': 'Switch to edit mode to fix the issue.',
'planView.error.writeFailed': 'Write failed',
@@ -1539,11 +1552,46 @@ export const dict = {
'diffView.hunk.unsupported': 'Staging individual hunks is not supported in this runtime.',
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Select a project to add notes and todos.',
'rightSidebar.contextNotesTodo.notes.title': 'Quick notes - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': 'Capture context, reminders, or links',
'rightSidebar.contextNotesTodo.todo.title': 'Todo',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count} item',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count} items',
'rightSidebar.contextNotesTodo.notes.addAria': 'Add note',
'rightSidebar.contextNotesTodo.notes.empty': 'No notes yet. Capture context, reminders, or links.',
'rightSidebar.contextNotesTodo.notes.actions.expand': 'Expand note',
'rightSidebar.contextNotesTodo.notes.actions.collapse': 'Collapse note',
'rightSidebar.contextNotesTodo.notes.actions.delete': 'Delete note',
'rightSidebar.contextNotesTodo.notes.actions.pin': 'Pin to agent context',
'rightSidebar.contextNotesTodo.notes.actions.unpin': 'Unpin from agent context',
'rightSidebar.contextNotesTodo.notes.source.selection': 'From chat',
'rightSidebar.contextNotesTodo.notes.source.agent': 'From agent',
'rightSidebar.contextNotesTodo.search.placeholder': 'Search',
'rightSidebar.contextNotesTodo.search.clear': 'Clear search',
'rightSidebar.contextNotesTodo.search.noResults': 'Nothing matches "{query}".',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': 'Failed to delete note',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': 'Failed to create note',
'rightSidebar.contextNotesTodo.tabs.notes': 'Notes',
'rightSidebar.contextNotesTodo.tabs.todos': 'Todo',
'rightSidebar.contextNotesTodo.tabs.plans': 'Plans',
'rightSidebar.contextNotesTodo.plans.actions.back': 'Back to plans',
'rightSidebar.contextNotesTodo.tabs.memory': 'Memory',
'rightSidebar.contextNotesTodo.sections.label': 'Project context sections',
'rightSidebar.contextNotesTodo.sections.resize': 'Resize sections sidebar',
'rightSidebar.contextNotesTodo.memory.scope.project': 'Project',
'rightSidebar.contextNotesTodo.memory.scope.label': 'Memory scope',
'rightSidebar.contextNotesTodo.memory.scope.global': 'About you',
'rightSidebar.contextNotesTodo.memory.type.fact': 'fact',
'rightSidebar.contextNotesTodo.memory.badge.new': 'new',
'rightSidebar.contextNotesTodo.memory.flagged': 'Withheld from the agent — reads as an instruction',
'rightSidebar.contextNotesTodo.memory.badge.changed': 'changed',
'rightSidebar.contextNotesTodo.memory.type.preference': 'preference',
'rightSidebar.contextNotesTodo.memory.type.reference': 'reference',
'rightSidebar.contextNotesTodo.memory.actions.delete': 'Forget this memory',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': 'Memory title',
'rightSidebar.contextNotesTodo.memory.actions.editBody': 'Memory text',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': 'Failed to save memory',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': 'Failed to forget memory',
'rightSidebar.contextNotesTodo.memory.empty.nothing': 'The agent has stored nothing here yet.',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': 'No stored memory matches your search.',
'rightSidebar.contextNotesTodo.memory.empty.noProject': 'Open a project to see what the agent remembers about it.',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': 'Stored memory could not be loaded. Nothing has been lost — try again.',
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Clear completed',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'Add a todo',
'rightSidebar.contextNotesTodo.todo.addAria': 'Add todo',
@@ -1554,13 +1602,9 @@ export const dict = {
'rightSidebar.contextNotesTodo.todo.actions.delete': 'Delete "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.send': 'Send "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Reorder "{text}"',
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Resize todo list',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'Send to current session',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'Send to new session',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'Send to new worktree session',
'rightSidebar.contextNotesTodo.plans.title': 'Plans',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} file',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} files',
'rightSidebar.contextNotesTodo.plans.importFromFile': 'Import plan from file',
'rightSidebar.contextNotesTodo.plans.empty': 'No saved plans yet.',
'rightSidebar.contextNotesTodo.plans.deletePlan': 'Delete plan',
@@ -1580,6 +1624,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': 'Todo sent to new session',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': 'Todo sent to new worktree session',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': 'Failed to send todo',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': 'Failed to update plan',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': 'Failed to delete plan',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': 'Plan file is empty',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': 'Failed to import plan',
@@ -2015,6 +2060,9 @@ export const dict = {
'chat.revert.toast.undo': 'Reverted to {preview}',
'chat.revert.toast.redo': 'Redone',
'chat.revert.toast.restored': 'Restored all messages',
'chat.toast.opencodeRestartInterrupted.title': 'Chat interrupted',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode restarted while a response was still running. Send a message to continue.',
'chat.toast.opencodeRestartInterrupted.openSession': 'Open session',
'chat.errorBoundary.title': 'Chat Error',
'chat.errorBoundary.description': 'The chat interface encountered an error. This might be due to a temporary network issue or corrupted message data.',
'chat.errorBoundary.sessionLabel': 'Session',
@@ -2982,13 +3030,15 @@ export const dict = {
'contextFileOpen.failure.unreadable': 'Failed to open file',
'quota.window.5h': '5-Hour',
'quota.window.7d': '7-Day Limit',
'quota.window.7dSonnet': '7-Day Sonnet Limit',
'quota.window.7dOpus': '7-Day Opus Limit',
'quota.window.weekly': 'Weekly Limit',
'quota.window.extraUsage': 'Extra Usage',
'quota.window.weekly': 'Weekly',
'quota.window.daily': 'Daily',
'quota.window.monthly': 'Monthly Limit',
'quota.window.monthly': 'Monthly',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Credits Balance',
'quota.window.monthlyCredits': 'Monthly Credits',
'quota.window.purchasedCredits': 'Purchased Credits',
'quota.window.freeCredits': 'Free Credits',
'quota.window.billingCycle': 'Billing Cycle',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3031,6 +3081,12 @@ export const dict = {
'chat.workStatus.subagent.askedQuestion': 'asked a question',
'chat.workStatus.section.contextBreakdown': 'Context sources',
'chat.workStatus.breakdown.skills': 'Skills',
'chat.workStatus.breakdown.pinnedNote': 'note',
'chat.workStatus.breakdown.unpin': 'Unpin from context',
'chat.workStatus.breakdown.pinnedPlan': 'plan',
'chat.workStatus.breakdown.memory': 'Agent memory',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} pinned',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} pinned',
'chat.workStatus.breakdown.mcp': 'MCP servers',
'chat.workStatus.action.openChanges': 'Open changes',
'chat.workStatus.action.openGit': 'Open Git panel',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Seguimiento de uso de OpenCode Go',
'settings.providers.page.openCodeGo.description': 'Conecta el panel de OpenCode Go para ver las cuotas móvil, semanal y mensual.',
@@ -864,16 +865,26 @@ export const settingsDict = {
"settings.skills.catalog.page.mode.manual": "Manual",
"settings.skills.catalog.page.mode.external": "Externo",
"settings.skills.catalog.page.title": "Catálogo de habilidades",
'settings.skills.catalog.page.subtitle': 'Instala skills listos desde repositorios curados o añade tu propia fuente.',
'settings.skills.catalog.page.section.sources': 'Fuentes',
'settings.skills.catalog.page.searchAllPlaceholder': 'Buscar skills en todas las fuentes…',
'settings.skills.catalog.page.search.clear': 'Borrar búsqueda',
'settings.skills.catalog.page.source.skillsCount': 'Skills: {count}',
'settings.skills.catalog.page.source.stars': 'Estrellas: {count}',
'settings.skills.catalog.page.source.updated': 'Actualizado {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Añadir tu propia fuente',
'settings.skills.catalog.page.source.addOwnDescription': 'Cualquier repositorio Git con skills',
'settings.skills.catalog.page.source.viewRepo': 'Abrir repositorio en GitHub',
'settings.skills.catalog.page.skill.viewOnGithub': 'Ver skill en GitHub',
'settings.skills.catalog.page.list.searchTitle': 'Resultados de búsqueda',
"settings.skills.catalog.page.section.sourceRepository": "Repositorio de origen",
"settings.skills.catalog.page.field.selectSourcePlaceholder": "Seleccionar origen",
"settings.skills.catalog.page.actions.refreshTitle": "Actualizar",
"settings.skills.catalog.page.actions.removeCatalogTitle": "Eliminar catálogo",
"settings.skills.catalog.page.actions.addCatalog": "Añadir catálogo",
"settings.skills.catalog.page.actions.removeCatalog": "Eliminar catálogo",
"settings.skills.catalog.page.actions.loadMoreSkills": "Cargar más habilidades",
"settings.skills.catalog.page.loading.catalog": "Cargando...",
"settings.skills.catalog.page.loading.skills": "Cargando habilidades...",
"settings.skills.catalog.page.loading.more": "Cargando...",
"settings.skills.catalog.page.foundCount": "{count} habilidad(es) encontrada(s)",
"settings.skills.catalog.page.error.catalogTitle": "Error del catálogo",
"settings.skills.catalog.page.empty.noSkillsTitle": "No se encontraron habilidades",
@@ -881,7 +892,6 @@ export const settingsDict = {
"settings.skills.catalog.page.badge.installed": "instalado ({scope})",
"settings.skills.catalog.page.badge.notInstallable": "no instalable",
"settings.skills.catalog.page.badge.unknown": "desconocido",
"settings.skills.catalog.page.byOwnerPrefix": "por",
"settings.skills.catalog.page.removeDialog.title": "Eliminar catálogo",
"settings.skills.catalog.page.removeDialog.description": "¿Estás seguro de que quieres eliminar este catálogo?",
"settings.openchamber.passkeys.title": "Claves de paso",
@@ -980,6 +990,9 @@ export const settingsDict = {
"settings.openchamber.tools.field.agentWebTool": "Herramienta OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolAria": "Activar la herramienta OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolInfo": "Permite que los agentes vean la página en el panel de navegador de OpenChamber e interactúen con ella: abrir una URL, leer el contenido, hacer clic, escribir, desplazarse y alternar entre diseño móvil y de escritorio. Añade una pequeña descripción de herramienta a cada sesión. Se aplica tras reiniciar OpenCode.",
"settings.openchamber.tools.field.agentMemoryTool": "Herramienta de memoria del agente",
"settings.openchamber.tools.field.agentMemoryToolAria": "Herramienta de memoria del agente",
"settings.openchamber.tools.field.agentMemoryToolInfo": "Permite que los agentes conserven lo aprendido entre sesiones, en dos almacenes: lo que es cierto sobre ti y lo que es cierto sobre cada proyecto. Las sesiones reciben los títulos guardados para que el agente pueda leer una entrada cuando resulte relevante. Al desactivarla se retiran la herramienta, la pestaña Memoria y el índice de sesión. Se aplica tras reiniciar OpenCode.",
"settings.openchamber.opencodeCli.tooltipPrefix": "Ruta absoluta opcional al",
"settings.openchamber.opencodeCli.tooltipSuffix": "ejecutable.",
"settings.openchamber.opencodeCli.field.binaryPath": "Ruta del ejecutable de OpenCode",
@@ -1160,6 +1173,7 @@ export const settingsDict = {
"settings.usage.page.header.providerUsage": "Uso de {provider}",
"settings.usage.page.header.refreshing": "Actualizando uso...",
"settings.usage.page.header.lastUpdated": "Última actualización: {time}",
"settings.usage.page.header.lastUpdatedWithPlan": "Plan: {plan} · Última actualización: {time}",
"settings.usage.page.options.showInWorkStatusAria": "Mostrar en el panel de estado del trabajo",
"settings.usage.page.options.showInWorkStatus": "Mostrar en el panel de estado del trabajo",
"settings.usage.page.options.showInWorkStatusTooltip": "Cuando esté habilitado, el uso de este proveedor será visible en el panel de estado del trabajo.",
@@ -1326,13 +1340,18 @@ export const settingsDict = {
"settings.providers.page.custom.title": "Proveedor personalizado",
"settings.providers.page.custom.editTitle": "Editar proveedor personalizado",
"settings.providers.page.custom.description": "Añade un proveedor compatible con OpenAI con URL base, credenciales y lista de modelos. Se guarda en la configuración de OpenCode para usarlo en el chat como cualquier otro proveedor.",
"settings.providers.page.custom.description": "Añade un proveedor con URL base, credenciales, lista de modelos y un protocolo de API compatible. Se guarda en la configuración de OpenCode para usarlo en el chat.",
"settings.providers.page.custom.field.providerID.label": "ID del proveedor",
"settings.providers.page.custom.field.providerID.placeholder": "mi-proveedor",
"settings.providers.page.custom.field.providerID.info": "Minúsculas, números, guiones y guiones bajos. Se usa como ID de proveedor de OpenCode.",
"settings.providers.page.custom.field.name.label": "Nombre visible",
"settings.providers.page.custom.field.name.placeholder": "Mi proveedor",
"settings.providers.page.custom.field.name.info": "Se muestra en los selectores de proveedor y modelo.",
"settings.providers.page.custom.field.protocol.label": "Protocolo de API",
"settings.providers.page.custom.field.protocol.info": "Elige el formato de solicitud que implementa esta API.",
"settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions",
"settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses",
"settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages",
"settings.providers.page.custom.field.baseURL.label": "URL base",
"settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1",
"settings.providers.page.custom.field.baseURL.info": "URL base de la API compatible con OpenAI. Debe empezar por http:// o https://.",
@@ -2133,4 +2152,5 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...thirdPartyIntegrationI18n.es,
} as const;
+70 -14
View File
@@ -998,6 +998,8 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.draft": "Borrador",
"gitView.pr.field.title": "Título",
"gitView.pr.githubNotConnected": "GitHub no está conectado",
"gitView.pr.history.merged": "La PR #{number} se fusionó en {base}.",
"gitView.pr.history.closed": "La PR #{number} se cerró.",
"gitView.pr.loadingDescription": "Cargando descripción...",
"gitView.pr.mergeMethod.merge": "Crear un merge commit",
"gitView.pr.mergeMethod.rebase": "Rebase y merge",
@@ -1187,12 +1189,12 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "El modelo pequeño no admite las respuestas estructuradas que necesita un recorrido.",
"contextRail.surface.plan.description": "Ver el plan actual",
"contextRail.surface.pr.description": "Crea, revisa y fusiona el pull request de la rama actual",
"contextRail.surface.notes.description": "Notas, tareas y planes del proyecto",
"contextRail.surface.notes.description": "Notas, tareas, planes y memoria del agente del proyecto",
"contextRail.surface.context.description": "Contexto de la sesión y uso de tokens",
"contextRail.surface.browser.description": "Navegador web integrado",
"contextRail.surface.preview.description": "Vista previa del servidor de desarrollo",
"contextRail.surface.chat.description": "Sesión abierta en paralelo",
"contextRail.surface.notes": "Notas del proyecto",
"contextRail.surface.notes": "Conocimiento del proyecto",
"contextRail.editorTree.toggle": "Alternar árbol de archivos",
"contextPanel.browser.open": "Abrir panel del navegador",
"contextPanel.browser.addressAria": "Dirección del navegador",
@@ -1286,6 +1288,14 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.toast.writeNotSupported": "La escritura no es compatible",
"sidebarFilesTree.toast.fileCreated": "Archivo creado",
"sidebarFilesTree.toast.operationFailed": "No se pudo completar la operación",
"sidebarFilesTree.toast.uploaded": "Archivos subidos",
"sidebarFilesTree.toast.uploadedWithoutConflicts": "Se subieron los archivos sin conflictos",
"sidebarFilesTree.toast.uploadFailed": "No se pudieron subir algunos archivos",
"sidebarFilesTree.drop.target": "Subir a {path}",
"sidebarFilesTree.drop.uploading": "Subiendo archivos a {path}",
"sidebarFilesTree.dialog.uploadConflicts.title": "¿Reemplazar los archivos existentes?",
"sidebarFilesTree.dialog.uploadConflicts.description": "Ya existen archivos con estos nombres en {path}. El reemplazo no se puede deshacer.",
"sidebarFilesTree.dialog.uploadConflicts.replace": "Reemplazar",
"sidebarFilesTree.toast.folderNameRequired": "El nombre de carpeta es obligatorio",
"sidebarFilesTree.toast.folderCreated": "Carpeta creada",
"sidebarFilesTree.toast.nameRequired": "El nombre es obligatorio",
@@ -1393,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
"contextUsage.mobile.usedTokens": "Tokens usados",
"contextUsage.mobile.contextLimit": "Límite de contexto",
"contextUsage.mobile.outputLimit": "Límite de salida",
"contextUsage.mobile.cost": "Costo",
"contextUsage.mobile.usage": "Uso",
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
"contextUsage.tooltip.contextLimit": "Límite de contexto: {tokens}",
"contextUsage.tooltip.outputLimit": "Límite de salida: {tokens}",
"contextUsage.tooltip.cost": "Costo: {cost}",
"contextSidebar.session.untitled": "Sesión sin título",
"contextSidebar.empty.openSession": "Abrir una sesión para inspeccionar el contexto.",
"contextSidebar.section.context": "Contexto",
@@ -1423,6 +1435,7 @@ export const dict: Record<I18nKey, string> = {
"planView.file.defaultName": "plan",
"planView.title.default": "Plan",
"planView.error.saveFailed": "No se pudo guardar",
"planView.error.loadFailed": "No se pudo cargar este plan",
"planView.error.previewUnavailable": "Vista previa no disponible",
"planView.error.switchToEditMode": "Cambia al modo de edición para resolver el problema.",
"planView.error.writeFailed": "No se pudo escribir",
@@ -1517,11 +1530,46 @@ export const dict: Record<I18nKey, string> = {
"diffView.hunk.unsupported": "Preparar fragmentos individuales no es compatible en este entorno.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "Plan",
"rightSidebar.contextNotesTodo.empty.selectProject": "Selecciona un proyecto para añadir notas y tareas pendientes.",
"rightSidebar.contextNotesTodo.notes.title": "Notas rápidas — {project}",
"rightSidebar.contextNotesTodo.notes.placeholder": "Captura contexto, recordatorios o enlaces",
"rightSidebar.contextNotesTodo.todo.title": "Tareas pendientes",
"rightSidebar.contextNotesTodo.todo.itemsSingle": "{count} elemento",
"rightSidebar.contextNotesTodo.todo.itemsPlural": "{count} elementos",
"rightSidebar.contextNotesTodo.notes.addAria": "Añadir nota",
"rightSidebar.contextNotesTodo.notes.empty": "Aún no hay notas. Guarda contexto, recordatorios o enlaces.",
"rightSidebar.contextNotesTodo.notes.actions.expand": "Expandir nota",
"rightSidebar.contextNotesTodo.notes.actions.collapse": "Contraer nota",
"rightSidebar.contextNotesTodo.notes.actions.delete": "Eliminar nota",
"rightSidebar.contextNotesTodo.notes.actions.pin": "Fijar al contexto del agente",
"rightSidebar.contextNotesTodo.notes.actions.unpin": "Quitar del contexto del agente",
"rightSidebar.contextNotesTodo.notes.source.selection": "Del chat",
"rightSidebar.contextNotesTodo.notes.source.agent": "Del agente",
"rightSidebar.contextNotesTodo.search.placeholder": "Buscar",
"rightSidebar.contextNotesTodo.search.clear": "Borrar búsqueda",
"rightSidebar.contextNotesTodo.search.noResults": "Nada coincide con \"{query}\".",
"rightSidebar.contextNotesTodo.toast.deleteNoteFailed": "No se pudo eliminar la nota",
"rightSidebar.contextNotesTodo.toast.createNoteFailed": "No se pudo crear la nota",
"rightSidebar.contextNotesTodo.tabs.notes": "Notas",
"rightSidebar.contextNotesTodo.tabs.todos": "Tareas",
"rightSidebar.contextNotesTodo.tabs.plans": "Planes",
"rightSidebar.contextNotesTodo.plans.actions.back": "Volver a los planes",
"rightSidebar.contextNotesTodo.tabs.memory": "Memoria",
"rightSidebar.contextNotesTodo.sections.label": "Secciones del contexto del proyecto",
"rightSidebar.contextNotesTodo.sections.resize": "Redimensionar la barra de secciones",
"rightSidebar.contextNotesTodo.memory.scope.project": "Proyecto",
"rightSidebar.contextNotesTodo.memory.scope.label": "Ámbito de la memoria",
"rightSidebar.contextNotesTodo.memory.scope.global": "Sobre ti",
"rightSidebar.contextNotesTodo.memory.type.fact": "hecho",
"rightSidebar.contextNotesTodo.memory.badge.new": "nuevo",
"rightSidebar.contextNotesTodo.memory.flagged": "Retenido del agente: parece una instrucción",
"rightSidebar.contextNotesTodo.memory.badge.changed": "cambiado",
"rightSidebar.contextNotesTodo.memory.type.preference": "preferencia",
"rightSidebar.contextNotesTodo.memory.type.reference": "referencia",
"rightSidebar.contextNotesTodo.memory.actions.delete": "Olvidar esta memoria",
"rightSidebar.contextNotesTodo.memory.actions.editTitle": "Título de la memoria",
"rightSidebar.contextNotesTodo.memory.actions.editBody": "Texto de la memoria",
"rightSidebar.contextNotesTodo.memory.toast.saveFailed": "No se pudo guardar la memoria",
"rightSidebar.contextNotesTodo.memory.toast.deleteFailed": "No se pudo olvidar la memoria",
"rightSidebar.contextNotesTodo.memory.empty.nothing": "El agente aún no ha guardado nada aquí.",
"rightSidebar.contextNotesTodo.memory.empty.noMatches": "Ninguna memoria guardada coincide con tu búsqueda.",
"rightSidebar.contextNotesTodo.memory.empty.noProject": "Abre un proyecto para ver qué recuerda el agente sobre él.",
"rightSidebar.contextNotesTodo.memory.empty.unavailable": "No se pudo cargar la memoria guardada. No se ha perdido nada: inténtalo de nuevo.",
"rightSidebar.contextNotesTodo.todo.clearCompleted": "Limpiar completadas",
"rightSidebar.contextNotesTodo.todo.inputPlaceholder": "Añade una tarea pendiente",
"rightSidebar.contextNotesTodo.todo.addAria": "Añadir tarea pendiente",
@@ -1532,13 +1580,9 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.todo.actions.delete": "Eliminar \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Reordenar \"{text}\"",
"rightSidebar.contextNotesTodo.todo.resizeAria": "Redimensionar lista de tareas",
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar a la sesión actual",
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a una nueva sesión",
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a una nueva sesión de worktree",
"rightSidebar.contextNotesTodo.plans.title": "Planes",
"rightSidebar.contextNotesTodo.plans.filesSingle": "{count} archivo",
"rightSidebar.contextNotesTodo.plans.filesPlural": "{count} archivos",
"rightSidebar.contextNotesTodo.plans.importFromFile": "Importar plan desde archivo",
"rightSidebar.contextNotesTodo.plans.empty": "Aún no hay plans guardados.",
"rightSidebar.contextNotesTodo.plans.deletePlan": "Eliminar plan",
@@ -1558,6 +1602,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.sentToNewSession": "Tarea enviada a una nueva sesión",
"rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession": "Tarea enviada a una nueva sesión de worktree",
"rightSidebar.contextNotesTodo.toast.sendTodoFailed": "No se pudo enviar la tarea",
"rightSidebar.contextNotesTodo.toast.updatePlanFailed": "No se pudo actualizar el plan",
"rightSidebar.contextNotesTodo.toast.deletePlanFailed": "No se pudo eliminar el plan",
"rightSidebar.contextNotesTodo.toast.planFileEmpty": "El archivo del plan está vacío",
"rightSidebar.contextNotesTodo.toast.importPlanFailed": "No se pudo importar el plan",
@@ -1993,6 +2038,9 @@ export const dict: Record<I18nKey, string> = {
"chat.revert.toast.undo": "Revertido a {preview}",
"chat.revert.toast.redo": "Rehecho",
"chat.revert.toast.restored": "Todos los mensajes restaurados",
"chat.toast.opencodeRestartInterrupted.title": "Conversación interrumpida",
"chat.toast.opencodeRestartInterrupted.description": "OpenCode se reinició mientras aún se estaba generando una respuesta. Envía un mensaje para continuar.",
"chat.toast.opencodeRestartInterrupted.openSession": "Abrir sesión",
"chat.errorBoundary.title": "Error en la conversación",
"chat.errorBoundary.description": "La interfaz de la conversación encontró un error. Esto podría deberse a un problema de red temporal o a datos de mensaje corruptos.",
"chat.errorBoundary.sessionLabel": "Sesión",
@@ -2983,13 +3031,15 @@ export const dict: Record<I18nKey, string> = {
"contextFileOpen.failure.unreadable": "Failed to open file",
"quota.window.5h": "5-Hour",
"quota.window.7d": "7-Day Limit",
"quota.window.7dSonnet": "7-Day Sonnet Limit",
"quota.window.7dOpus": "7-Day Opus Limit",
"quota.window.weekly": "Weekly Limit",
"quota.window.extraUsage": "Uso adicional",
"quota.window.weekly": "Semanal",
"quota.window.daily": "Daily",
"quota.window.monthly": "Monthly Limit",
"quota.window.monthly": "Mensual",
"quota.window.credits": "Credits",
"quota.window.creditsBalance": "Credits Balance",
"quota.window.monthlyCredits": "Créditos mensuales",
"quota.window.purchasedCredits": "Créditos comprados",
"quota.window.freeCredits": "Créditos gratuitos",
"quota.window.billingCycle": "Billing Cycle",
"quota.window.auto": "Auto",
"quota.window.api": "API",
@@ -3032,6 +3082,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': 'hizo una pregunta',
'chat.workStatus.section.contextBreakdown': 'Fuentes de contexto',
'chat.workStatus.breakdown.skills': 'Habilidades',
'chat.workStatus.breakdown.pinnedNote': 'nota',
'chat.workStatus.breakdown.unpin': 'Dejar de fijar al contexto',
'chat.workStatus.breakdown.pinnedPlan': 'plan',
'chat.workStatus.breakdown.memory': 'Memoria del agente',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} fijado',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} fijados',
'chat.workStatus.breakdown.mcp': 'Servidores MCP',
'chat.workStatus.action.openChanges': 'Abrir cambios',
'chat.workStatus.action.openGit': 'Abrir panel de Git',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Suivi de lutilisation dOpenCode Go',
'settings.providers.page.openCodeGo.description': 'Connectez le tableau de bord OpenCode Go pour afficher les quotas glissant, hebdomadaire et mensuel.',
@@ -782,16 +783,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': 'Manuel',
'settings.skills.catalog.page.mode.external': 'Externe',
'settings.skills.catalog.page.title': 'Catalogue de skills',
'settings.skills.catalog.page.subtitle': "Installez des skills prêts à l'emploi depuis des dépôts curatés ou ajoutez votre propre source.",
'settings.skills.catalog.page.section.sources': 'Sources',
'settings.skills.catalog.page.searchAllPlaceholder': 'Rechercher des skills dans toutes les sources…',
'settings.skills.catalog.page.search.clear': 'Effacer la recherche',
'settings.skills.catalog.page.source.skillsCount': 'Skills : {count}',
'settings.skills.catalog.page.source.stars': 'Étoiles : {count}',
'settings.skills.catalog.page.source.updated': 'Mis à jour {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Ajouter votre propre source',
'settings.skills.catalog.page.source.addOwnDescription': "N'importe quel dépôt Git avec des skills",
'settings.skills.catalog.page.source.viewRepo': 'Ouvrir le dépôt sur GitHub',
'settings.skills.catalog.page.skill.viewOnGithub': 'Voir le skill sur GitHub',
'settings.skills.catalog.page.list.searchTitle': 'Résultats de recherche',
'settings.skills.catalog.page.section.sourceRepository': 'Dépôt source',
'settings.skills.catalog.page.field.selectSourcePlaceholder': 'Sélectionnez la source',
'settings.skills.catalog.page.actions.refreshTitle': 'Rafraîchir',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'Supprimer le catalogue',
'settings.skills.catalog.page.actions.addCatalog': 'Ajouter un catalogue',
'settings.skills.catalog.page.actions.removeCatalog': 'Supprimer le catalogue',
'settings.skills.catalog.page.actions.loadMoreSkills': 'Charger plus de skills',
'settings.skills.catalog.page.loading.catalog': 'Chargement...',
'settings.skills.catalog.page.loading.skills': 'Chargement des skills...',
'settings.skills.catalog.page.loading.more': 'Chargement...',
'settings.skills.catalog.page.foundCount': '{count} skill(s) trouvé(s)',
'settings.skills.catalog.page.error.catalogTitle': 'Erreur de catalogue',
'settings.skills.catalog.page.empty.noSkillsTitle': 'Aucun skill trouvé',
@@ -799,7 +810,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': 'installé ({scope})',
'settings.skills.catalog.page.badge.notInstallable': 'non installable',
'settings.skills.catalog.page.badge.unknown': 'inconnu',
'settings.skills.catalog.page.byOwnerPrefix': 'par',
'settings.skills.catalog.page.removeDialog.title': 'Supprimer le catalogue',
'settings.skills.catalog.page.removeDialog.description': 'Êtes-vous sûr de vouloir supprimer ce catalogue ?',
'settings.openchamber.passkeys.title': 'Mots-clés',
@@ -898,6 +908,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'Outil OpenChamber Web',
'settings.openchamber.tools.field.agentWebToolAria': 'Activer loutil OpenChamber Web',
'settings.openchamber.tools.field.agentWebToolInfo': 'Laissez les agents consulter la page dans le panneau navigateur dOpenChamber et interagir avec elle : ouvrir une URL, lire le contenu, cliquer, saisir du texte, faire défiler et basculer entre les mises en page mobile et bureau. Ajoute une courte description doutil à chaque session. Appliqué après le redémarrage dOpenCode.',
'settings.openchamber.tools.field.agentMemoryTool': 'Outil de mémoire de lagent',
'settings.openchamber.tools.field.agentMemoryToolAria': 'Outil de mémoire de lagent',
'settings.openchamber.tools.field.agentMemoryToolInfo': 'Permet aux agents de conserver ce quils apprennent dune session à lautre, dans deux stockages : ce qui est vrai à votre sujet et ce qui est vrai pour chaque projet. Les sessions reçoivent les titres enregistrés afin que lagent puisse lire une entrée pertinente. La désactivation retire loutil, longlet Mémoire et lindex de session. Appliqué après le redémarrage dOpenCode.',
'settings.openchamber.opencodeCli.tooltipPrefix': 'Chemin absolu facultatif vers le',
'settings.openchamber.opencodeCli.tooltipSuffix': 'binaire.',
'settings.openchamber.opencodeCli.field.binaryPath': 'Chemin binaire OpenCode',
@@ -1078,6 +1091,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': 'Utilisation de {provider}',
'settings.usage.page.header.refreshing': 'Utilisation rafraîchissante...',
'settings.usage.page.header.lastUpdated': 'Dernière mise à jour : {time}',
'settings.usage.page.header.lastUpdatedWithPlan': 'Forfait : {plan} · Dernière mise à jour : {time}',
'settings.usage.page.options.showInWorkStatusAria': 'Afficher dans le panneau d’état du travail',
'settings.usage.page.options.showInWorkStatus': 'Afficher dans le panneau d’état du travail',
'settings.usage.page.options.showInWorkStatusTooltip': 'Lorsquelle est activée, lutilisation de ce fournisseur sera visible dans le panneau d’état du travail.',
@@ -1244,13 +1258,18 @@ export const settingsDict = {
'settings.providers.page.custom.title': 'Fournisseur personnalisé',
'settings.providers.page.custom.editTitle': 'Modifier le fournisseur personnalisé',
'settings.providers.page.custom.description': 'Ajoutez un fournisseur compatible OpenAI avec une URL de base, des identifiants et une liste de modèles. Enregistré dans la configuration OpenCode pour lutiliser dans le chat comme les autres fournisseurs.',
'settings.providers.page.custom.description': 'Ajoutez un fournisseur avec une URL de base, des identifiants, une liste de modèles et un protocole API pris en charge. Enregistré dans la configuration OpenCode pour le chat.',
'settings.providers.page.custom.field.providerID.label': 'ID du fournisseur',
'settings.providers.page.custom.field.providerID.placeholder': 'mon-fournisseur',
'settings.providers.page.custom.field.providerID.info': 'Minuscules, chiffres, tirets et underscores. Utilisé comme ID de fournisseur OpenCode.',
'settings.providers.page.custom.field.name.label': 'Nom affiché',
'settings.providers.page.custom.field.name.placeholder': 'Mon fournisseur',
'settings.providers.page.custom.field.name.info': 'Affiché dans les sélecteurs de fournisseur et de modèle.',
'settings.providers.page.custom.field.protocol.label': 'Protocole API',
'settings.providers.page.custom.field.protocol.info': 'Choisissez le format de requête implémenté par cette API.',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': 'URL de base',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'URL de base de lAPI compatible OpenAI. Doit commencer par http:// ou https://.',
@@ -2133,4 +2152,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n.fr,
} as const;
+70 -14
View File
@@ -817,6 +817,8 @@ export const dict = {
'gitView.pr.field.draft': 'Brouillon',
'gitView.pr.field.title': 'Titre',
'gitView.pr.githubNotConnected': 'GitHub n\'est pas connecté',
'gitView.pr.history.merged': 'La PR #{number} a été fusionnée dans {base}.',
'gitView.pr.history.closed': 'La PR #{number} a été fermée.',
'gitView.pr.loadingDescription': 'Chargement de la description de la PR...',
'gitView.pr.mergeMethod.merge': 'Créer un commit de fusion',
'gitView.pr.mergeMethod.rebase': 'Rebase et fusionner',
@@ -1006,12 +1008,12 @@ export const dict = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Le petit modèle ne prend pas en charge les réponses structurées nécessaires à un parcours.',
'contextRail.surface.plan.description': 'Voir le plan actuel',
'contextRail.surface.pr.description': 'Créer, relire et fusionner la pull request de la branche actuelle',
'contextRail.surface.notes.description': 'Notes, tâches et plans du projet',
'contextRail.surface.notes.description': 'Notes, tâches, plans et mémoire de lagent pour le projet',
'contextRail.surface.context.description': 'Contexte de session et utilisation des tokens',
'contextRail.surface.browser.description': 'Navigateur web intégré',
'contextRail.surface.preview.description': 'Aperçu du serveur de développement',
'contextRail.surface.chat.description': 'Session ouverte côte à côte',
'contextRail.surface.notes': 'Notes du projet',
'contextRail.surface.notes': 'Connaissances du projet',
'contextRail.editorTree.toggle': 'Afficher/masquer larborescence de fichiers',
'contextPanel.browser.open': 'Ouvrir le panneau du navigateur',
'contextPanel.browser.addressAria': 'Adresse du navigateur',
@@ -1087,6 +1089,14 @@ export const dict = {
'sidebarFilesTree.toast.writeNotSupported': 'Écriture non prise en charge',
'sidebarFilesTree.toast.fileCreated': 'Fichier créé',
'sidebarFilesTree.toast.operationFailed': 'L\'opération a échoué',
'sidebarFilesTree.toast.uploaded': 'Fichiers téléversés',
'sidebarFilesTree.toast.uploadedWithoutConflicts': 'Les fichiers sans conflit ont été téléversés',
'sidebarFilesTree.toast.uploadFailed': 'Certains fichiers nont pas pu être téléversés',
'sidebarFilesTree.drop.target': 'Téléverser dans {path}',
'sidebarFilesTree.drop.uploading': 'Téléversement des fichiers dans {path}',
'sidebarFilesTree.dialog.uploadConflicts.title': 'Remplacer les fichiers existants ?',
'sidebarFilesTree.dialog.uploadConflicts.description': 'Des fichiers portant ces noms existent déjà dans {path}. Leur remplacement est irréversible.',
'sidebarFilesTree.dialog.uploadConflicts.replace': 'Remplacer',
'sidebarFilesTree.toast.folderNameRequired': 'Le nom du dossier est requis',
'sidebarFilesTree.toast.folderCreated': 'Dossier créé',
'sidebarFilesTree.toast.nameRequired': 'Le nom est requis',
@@ -1192,10 +1202,12 @@ export const dict = {
'contextUsage.mobile.usedTokens': 'Jetons utilisés',
'contextUsage.mobile.contextLimit': 'Limite de contexte',
'contextUsage.mobile.outputLimit': 'Limite de sortie',
'contextUsage.mobile.cost': 'Coût',
'contextUsage.mobile.usage': 'Usage',
'contextUsage.tooltip.usedTokens': 'Jetons utilisés : {tokens}',
'contextUsage.tooltip.contextLimit': 'Limite de contexte : {tokens}',
'contextUsage.tooltip.outputLimit': 'Limite de sortie : {tokens}',
'contextUsage.tooltip.cost': 'Coût : {cost}',
'contextSidebar.session.untitled': 'Session sans titre',
'contextSidebar.empty.openSession': 'Ouvrez une session pour inspecter le contexte.',
'contextSidebar.section.context': 'Contexte',
@@ -1222,6 +1234,7 @@ export const dict = {
'planView.file.defaultName': 'plan',
'planView.title.default': 'Plan',
'planView.error.saveFailed': 'Échec de l\'enregistrement',
'planView.error.loadFailed': 'Impossible de charger ce plan',
'planView.error.previewUnavailable': 'Aperçu indisponible',
'planView.error.switchToEditMode': 'Passez en mode édition pour résoudre le problème.',
'planView.error.writeFailed': 'Échec de l\'écriture',
@@ -1304,11 +1317,46 @@ export const dict = {
'diffView.hunk.unsupported': "La préparation de sections individuelles n'est pas prise en charge dans cet environnement.",
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Sélectionnez un projet pour ajouter des notes et des tâches.',
'rightSidebar.contextNotesTodo.notes.title': 'Notes rapides - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': 'Capturez le contexte, les rappels ou les liens',
'rightSidebar.contextNotesTodo.todo.title': 'Faire',
'rightSidebar.contextNotesTodo.todo.itemsSingle': 'Article {count}',
'rightSidebar.contextNotesTodo.todo.itemsPlural': 'Articles {count}',
'rightSidebar.contextNotesTodo.notes.addAria': 'Ajouter une note',
'rightSidebar.contextNotesTodo.notes.empty': 'Aucune note pour le moment. Notez du contexte, des rappels ou des liens.',
'rightSidebar.contextNotesTodo.notes.actions.expand': 'Développer la note',
'rightSidebar.contextNotesTodo.notes.actions.collapse': 'Réduire la note',
'rightSidebar.contextNotesTodo.notes.actions.delete': 'Supprimer la note',
'rightSidebar.contextNotesTodo.notes.actions.pin': 'Épingler au contexte de l\'agent',
'rightSidebar.contextNotesTodo.notes.actions.unpin': 'Détacher du contexte de l\'agent',
'rightSidebar.contextNotesTodo.notes.source.selection': 'Depuis le chat',
'rightSidebar.contextNotesTodo.notes.source.agent': 'Depuis l\'agent',
'rightSidebar.contextNotesTodo.search.placeholder': 'Rechercher',
'rightSidebar.contextNotesTodo.search.clear': 'Effacer la recherche',
'rightSidebar.contextNotesTodo.search.noResults': 'Aucun résultat pour "{query}".',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': 'Échec de la suppression de la note',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': 'Échec de la création de la note',
'rightSidebar.contextNotesTodo.tabs.notes': 'Notes',
'rightSidebar.contextNotesTodo.tabs.todos': 'Tâches',
'rightSidebar.contextNotesTodo.tabs.plans': 'Plans',
'rightSidebar.contextNotesTodo.plans.actions.back': 'Retour aux plans',
'rightSidebar.contextNotesTodo.tabs.memory': 'Mémoire',
'rightSidebar.contextNotesTodo.sections.label': 'Sections du contexte du projet',
'rightSidebar.contextNotesTodo.sections.resize': 'Redimensionner la barre des sections',
'rightSidebar.contextNotesTodo.memory.scope.project': 'Projet',
'rightSidebar.contextNotesTodo.memory.scope.label': 'Portée de la mémoire',
'rightSidebar.contextNotesTodo.memory.scope.global': 'À votre sujet',
'rightSidebar.contextNotesTodo.memory.type.fact': 'fait',
'rightSidebar.contextNotesTodo.memory.badge.new': 'nouveau',
'rightSidebar.contextNotesTodo.memory.flagged': 'Retenu — se lit comme une instruction',
'rightSidebar.contextNotesTodo.memory.badge.changed': 'modifié',
'rightSidebar.contextNotesTodo.memory.type.preference': 'préférence',
'rightSidebar.contextNotesTodo.memory.type.reference': 'référence',
'rightSidebar.contextNotesTodo.memory.actions.delete': 'Oublier cette mémoire',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': 'Titre de la mémoire',
'rightSidebar.contextNotesTodo.memory.actions.editBody': 'Texte de la mémoire',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': 'Impossible denregistrer la mémoire',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': 'Impossible doublier la mémoire',
'rightSidebar.contextNotesTodo.memory.empty.nothing': 'Lagent na encore rien enregistré ici.',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': 'Aucune mémoire enregistrée ne correspond à votre recherche.',
'rightSidebar.contextNotesTodo.memory.empty.noProject': 'Ouvrez un projet pour voir ce que lagent en retient.',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': 'Impossible de charger la mémoire enregistrée. Rien nest perdu — réessayez.',
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Effacer terminé',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'Ajouter une tâche',
'rightSidebar.contextNotesTodo.todo.addAria': 'Ajouter une tâche',
@@ -1319,13 +1367,9 @@ export const dict = {
'rightSidebar.contextNotesTodo.todo.actions.delete': 'Supprimer "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.send': 'Envoyer "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Récommander "{text}"',
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Redimensionner la liste de tâches',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'Envoyer à la session en cours',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'Envoyer à une nouvelle session',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'Envoyer à une nouvelle session Worktree',
'rightSidebar.contextNotesTodo.plans.title': 'Forfaits',
'rightSidebar.contextNotesTodo.plans.filesSingle': 'Fichier {count}',
'rightSidebar.contextNotesTodo.plans.filesPlural': 'Fichiers {count}',
'rightSidebar.contextNotesTodo.plans.importFromFile': 'Importer un plan à partir d\'un fichier',
'rightSidebar.contextNotesTodo.plans.empty': 'Aucun plan enregistré pour l\'instant.',
'rightSidebar.contextNotesTodo.plans.deletePlan': 'Supprimer le forfait',
@@ -1345,6 +1389,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': 'Todo envoyé à une nouvelle session',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': 'Todo envoyé à une nouvelle session Worktree',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': 'Échec de l\'envoi de la tâche',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': 'Échec de la mise à jour du plan',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': 'Échec de la suppression du plan',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': 'Le fichier de plan est vide',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': 'Échec de l\'importation du plan',
@@ -1757,6 +1802,9 @@ export const dict = {
'chat.revert.toast.undo': 'Revenu à {preview}',
'chat.revert.toast.redo': 'Refait',
'chat.revert.toast.restored': 'Restauré tous les messages',
'chat.toast.opencodeRestartInterrupted.title': 'Discussion interrompue',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode a redémarré alors quune réponse était encore en cours. Envoyez un message pour continuer.',
'chat.toast.opencodeRestartInterrupted.openSession': 'Ouvrir la session',
'chat.errorBoundary.title': 'Erreur de discussion',
'chat.errorBoundary.description': 'L\'interface de discussion a rencontré une erreur. Cela peut être dû à un problème de réseau temporaire ou à des données de message corrompues.',
'chat.errorBoundary.sessionLabel': 'Session',
@@ -2675,13 +2723,15 @@ export const dict = {
'contextFileOpen.failure.unreadable': 'Impossible douvrir le fichier',
'quota.window.5h': '5 heures',
'quota.window.7d': 'Limite sur 7 jours',
'quota.window.7dSonnet': 'Limite Sonnet sur 7 jours',
'quota.window.7dOpus': 'Limite Opus sur 7 jours',
'quota.window.weekly': 'Limite hebdomadaire',
'quota.window.extraUsage': 'Utilisation supplémentaire',
'quota.window.weekly': 'Hebdomadaire',
'quota.window.daily': 'Quotidien',
'quota.window.monthly': 'Limite mensuelle',
'quota.window.monthly': 'Mensuel',
'quota.window.credits': 'Crédits',
'quota.window.creditsBalance': 'Solde de crédits',
'quota.window.monthlyCredits': 'Crédits mensuels',
'quota.window.purchasedCredits': 'Crédits achetés',
'quota.window.freeCredits': 'Crédits gratuits',
'quota.window.billingCycle': 'Cycle de facturation',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3029,6 +3079,12 @@ export const dict = {
'chat.workStatus.subagent.askedQuestion': 'a posé une question',
'chat.workStatus.section.contextBreakdown': 'Sources de contexte',
'chat.workStatus.breakdown.skills': 'Compétences',
'chat.workStatus.breakdown.pinnedNote': 'note',
'chat.workStatus.breakdown.unpin': 'Détacher du contexte',
'chat.workStatus.breakdown.pinnedPlan': 'plan',
'chat.workStatus.breakdown.memory': 'Mémoire de lagent',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} épinglé',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} épinglés',
'chat.workStatus.breakdown.mcp': 'Serveurs MCP',
'chat.workStatus.action.openChanges': 'Ouvrir les modifications',
'chat.workStatus.action.openGit': 'Ouvrir le panneau Git',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 使用量追跡',
'settings.providers.page.openCodeGo.description': 'OpenCode Go ダッシュボードを接続して、ローリング、週間、月間のクォータを表示します。',
@@ -897,16 +898,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': '手動',
'settings.skills.catalog.page.mode.external': '外部',
'settings.skills.catalog.page.title': 'スキルカタログ',
'settings.skills.catalog.page.subtitle': 'キュレーションされたリポジトリからすぐ使えるスキルをインストール、または独自のソースを追加。',
'settings.skills.catalog.page.section.sources': 'ソース',
'settings.skills.catalog.page.searchAllPlaceholder': 'すべてのソースのスキルを検索…',
'settings.skills.catalog.page.search.clear': '検索をクリア',
'settings.skills.catalog.page.source.skillsCount': 'スキル数: {count}',
'settings.skills.catalog.page.source.stars': 'スター: {count}',
'settings.skills.catalog.page.source.updated': '更新: {time}',
'settings.skills.catalog.page.source.addOwnTitle': '独自のソースを追加',
'settings.skills.catalog.page.source.addOwnDescription': 'スキルを含む任意の Git リポジトリ',
'settings.skills.catalog.page.source.viewRepo': 'GitHub でリポジトリを開く',
'settings.skills.catalog.page.skill.viewOnGithub': 'GitHub でスキルを表示',
'settings.skills.catalog.page.list.searchTitle': '検索結果',
'settings.skills.catalog.page.section.sourceRepository': 'ソースリポジトリ',
'settings.skills.catalog.page.field.selectSourcePlaceholder': 'ソースを選択',
'settings.skills.catalog.page.actions.refreshTitle': '更新',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'カタログを削除',
'settings.skills.catalog.page.actions.addCatalog': 'カタログを追加',
'settings.skills.catalog.page.actions.removeCatalog': 'カタログを削除',
'settings.skills.catalog.page.actions.loadMoreSkills': 'さらに Skill を読み込む',
'settings.skills.catalog.page.loading.catalog': '読み込み中...',
'settings.skills.catalog.page.loading.skills': 'Skill を読み込み中...',
'settings.skills.catalog.page.loading.more': '読み込み中...',
'settings.skills.catalog.page.foundCount': '{count} 個の Skill が見つかりました',
'settings.skills.catalog.page.error.catalogTitle': 'カタログエラー',
'settings.skills.catalog.page.empty.noSkillsTitle': 'Skill が見つかりません',
@@ -914,7 +925,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': 'インストール済み ({scope})',
'settings.skills.catalog.page.badge.notInstallable': 'インストール不可',
'settings.skills.catalog.page.badge.unknown': '不明',
'settings.skills.catalog.page.byOwnerPrefix': '提供',
'settings.skills.catalog.page.removeDialog.title': 'カタログを削除',
'settings.skills.catalog.page.removeDialog.description': 'このカタログを削除してもよろしいですか?',
'settings.openchamber.passkeys.title': 'パスキー',
@@ -1013,6 +1023,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber Web ツール',
'settings.openchamber.tools.field.agentWebToolAria': 'OpenChamber Web ツールを有効にする',
'settings.openchamber.tools.field.agentWebToolInfo': 'エージェントが OpenChamber のブラウザーパネルでページを確認し操作できるようにします。URL を開く、内容を読む、クリック、入力、スクロール、モバイルとデスクトップのレイアウト切り替えが可能です。各セッションに小さなツール説明が追加されます。OpenCode の再起動後に適用されます。',
'settings.openchamber.tools.field.agentMemoryTool': 'エージェントメモリツール',
'settings.openchamber.tools.field.agentMemoryToolAria': 'エージェントメモリツール',
'settings.openchamber.tools.field.agentMemoryToolInfo': 'エージェントが学んだことをセッションをまたいで保持できるようにします。保存先は 2 つで、ユーザーについての事実と、各プロジェクトについての事実です。セッションには保存済みのタイトルが渡され、関連する項目をエージェントが読み出せます。オフにするとツール、メモリタブ、セッションインデックスがすべてなくなります。OpenCode の再起動後に反映されます。',
'settings.openchamber.opencodeCli.tooltipPrefix': '以下への絶対パス(任意):',
'settings.openchamber.opencodeCli.tooltipSuffix': 'バイナリ。',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode バイナリパス',
@@ -1193,6 +1206,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': '{provider} の使用量',
'settings.usage.page.header.refreshing': '使用量を更新中...',
'settings.usage.page.header.lastUpdated': '最終更新: {time}',
'settings.usage.page.header.lastUpdatedWithPlan': 'プラン: {plan} · 最終更新: {time}',
'settings.usage.page.options.showInWorkStatusAria': '作業ステータスパネルに表示',
'settings.usage.page.options.showInWorkStatus': '作業ステータスパネルに表示',
'settings.usage.page.options.showInWorkStatusTooltip': '有効にすると、このプロバイダーの使用量が作業ステータスパネルに表示されます。',
@@ -1359,13 +1373,18 @@ export const settingsDict = {
'settings.providers.page.custom.title': 'カスタムプロバイダー',
'settings.providers.page.custom.editTitle': 'カスタムプロバイダーを編集',
'settings.providers.page.custom.description': 'ベース URL認証情報モデル一覧を指定して、OpenAI 互換プロバイダーを追加します。OpenCode 設定に保存され、他のプロバイダーと同様にチャットで使えます。',
'settings.providers.page.custom.description': 'ベース URL認証情報モデル一覧、対応 API プロトコルを指定してプロバイダーを追加します。チャットで使えるよう OpenCode 設定に保存されます。',
'settings.providers.page.custom.field.providerID.label': 'プロバイダー ID',
'settings.providers.page.custom.field.providerID.placeholder': 'my-provider',
'settings.providers.page.custom.field.providerID.info': '小文字・数字・ハイフン・アンダースコア。OpenCode のプロバイダー ID として使われます。',
'settings.providers.page.custom.field.name.label': '表示名',
'settings.providers.page.custom.field.name.placeholder': 'My Provider',
'settings.providers.page.custom.field.name.info': 'プロバイダーおよびモデル選択に表示されます。',
'settings.providers.page.custom.field.protocol.label': 'API プロトコル',
'settings.providers.page.custom.field.protocol.info': 'この API が実装しているリクエスト形式を選択します。',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': 'ベース URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'OpenAI 互換 API のベース URL。http:// または https:// で始めてください。',
@@ -2133,4 +2152,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'エージェントが応答している間にフォローアップメッセージで Enter を押したときの動作を選択します。',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'ステア',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'キュー',
...thirdPartyIntegrationI18n.ja,
} as const;
+70 -14
View File
@@ -994,6 +994,8 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.draft': '下書き',
'gitView.pr.field.title': 'タイトル',
'gitView.pr.githubNotConnected': 'GitHubが接続されていません',
'gitView.pr.history.merged': 'PR #{number} は {base} にマージされました。',
'gitView.pr.history.closed': 'PR #{number} はクローズされました。',
'gitView.pr.loadingDescription': '説明を読み込み中...',
'gitView.pr.mergeMethod.merge': 'マージコミットを作成',
'gitView.pr.mergeMethod.rebase': 'リベースしてマージ',
@@ -1183,12 +1185,12 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'スモールモデルはウォークスルーに必要な構造化応答をサポートしていません。',
'contextRail.surface.plan.description': '現在のプランを表示',
'contextRail.surface.pr.description': '現在のブランチのプルリクエストを作成・確認・マージ',
'contextRail.surface.notes.description': 'プロジェクトのノート・ToDoプラン',
'contextRail.surface.notes.description': 'プロジェクトのメモ、ToDoプラン、エージェントのメモリ',
'contextRail.surface.context.description': 'セッションのコンテキストとトークン使用量',
'contextRail.surface.browser.description': '内蔵ウェブブラウザ',
'contextRail.surface.preview.description': '開発サーバーのプレビュー',
'contextRail.surface.chat.description': '並べて開いたセッション',
'contextRail.surface.notes': 'プロジェクトノート',
'contextRail.surface.notes': 'プロジェクトナレッジ',
'contextRail.editorTree.toggle': 'ファイルツリーの表示切替',
'contextPanel.browser.open': 'ブラウザパネルを開く',
'contextPanel.browser.addressAria': 'ブラウザアドレス',
@@ -1316,6 +1318,14 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.toast.writeNotSupported': '書き込みはサポートされていません',
'sidebarFilesTree.toast.fileCreated': 'ファイルを作成しました',
'sidebarFilesTree.toast.operationFailed': '操作に失敗しました',
'sidebarFilesTree.toast.uploaded': 'ファイルをアップロードしました',
'sidebarFilesTree.toast.uploadedWithoutConflicts': '競合のないファイルをアップロードしました',
'sidebarFilesTree.toast.uploadFailed': '一部のファイルをアップロードできませんでした',
'sidebarFilesTree.drop.target': '{path} にアップロード',
'sidebarFilesTree.drop.uploading': '{path} にファイルをアップロードしています',
'sidebarFilesTree.dialog.uploadConflicts.title': '既存のファイルを置き換えますか?',
'sidebarFilesTree.dialog.uploadConflicts.description': '同じ名前のファイルが {path} に既に存在します。置き換えは元に戻せません。',
'sidebarFilesTree.dialog.uploadConflicts.replace': '置き換える',
'sidebarFilesTree.toast.folderNameRequired': 'フォルダ名が必要です',
'sidebarFilesTree.toast.folderCreated': 'フォルダを作成しました',
'sidebarFilesTree.toast.nameRequired': '名前が必要です',
@@ -1423,10 +1433,12 @@ export const dict: Record<I18nKey, string> = {
'contextUsage.mobile.usedTokens': '使用トークン',
'contextUsage.mobile.contextLimit': 'コンテキスト制限',
'contextUsage.mobile.outputLimit': '出力制限',
'contextUsage.mobile.cost': 'コスト',
'contextUsage.mobile.usage': '使用量',
'contextUsage.tooltip.usedTokens': '使用トークン: {tokens}',
'contextUsage.tooltip.contextLimit': 'コンテキスト制限: {tokens}',
'contextUsage.tooltip.outputLimit': '出力制限: {tokens}',
'contextUsage.tooltip.cost': 'コスト: {cost}',
'contextSidebar.session.untitled': '無題のセッション',
'contextSidebar.empty.openSession': 'セッションを開いてコンテキストを確認します。',
'contextSidebar.section.context': 'コンテキスト',
@@ -1453,6 +1465,7 @@ export const dict: Record<I18nKey, string> = {
'planView.file.defaultName': '計画',
'planView.title.default': '計画',
'planView.error.saveFailed': '保存に失敗しました',
'planView.error.loadFailed': 'この計画を読み込めませんでした',
'planView.error.previewUnavailable': 'プレビューは利用できません',
'planView.error.switchToEditMode': '編集モードに切り替えて問題を修正してください。',
'planView.error.writeFailed': '書き込みに失敗しました',
@@ -1535,11 +1548,46 @@ export const dict: Record<I18nKey, string> = {
'chat.autoReview.actions.stop': '停止',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '計画',
'rightSidebar.contextNotesTodo.empty.selectProject': 'プロジェクトを選択してメモとTODOを追加します。',
'rightSidebar.contextNotesTodo.notes.title': 'クイックメモ - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': 'コンテキスト、リマインダー、リンクを記録',
'rightSidebar.contextNotesTodo.todo.title': 'TODO',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count}項目',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count}項目',
'rightSidebar.contextNotesTodo.notes.addAria': 'ノートを追加',
'rightSidebar.contextNotesTodo.notes.empty': 'ノートはまだありません。文脈やメモ、リンクを残せます。',
'rightSidebar.contextNotesTodo.notes.actions.expand': 'ノートを展開',
'rightSidebar.contextNotesTodo.notes.actions.collapse': 'ノートを折りたたむ',
'rightSidebar.contextNotesTodo.notes.actions.delete': 'ノートを削除',
'rightSidebar.contextNotesTodo.notes.actions.pin': 'エージェントのコンテキストにピン留め',
'rightSidebar.contextNotesTodo.notes.actions.unpin': 'エージェントのコンテキストからピン留めを解除',
'rightSidebar.contextNotesTodo.notes.source.selection': 'チャットから',
'rightSidebar.contextNotesTodo.notes.source.agent': 'エージェントから',
'rightSidebar.contextNotesTodo.search.placeholder': '検索',
'rightSidebar.contextNotesTodo.search.clear': '検索をクリア',
'rightSidebar.contextNotesTodo.search.noResults': '「{query}」に一致するものはありません。',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': 'ノートを削除できませんでした',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': 'ノートを作成できませんでした',
'rightSidebar.contextNotesTodo.tabs.notes': 'ノート',
'rightSidebar.contextNotesTodo.tabs.todos': 'Todo',
'rightSidebar.contextNotesTodo.tabs.plans': '計画',
'rightSidebar.contextNotesTodo.plans.actions.back': 'プラン一覧に戻る',
'rightSidebar.contextNotesTodo.tabs.memory': 'メモリ',
'rightSidebar.contextNotesTodo.sections.label': 'プロジェクトコンテキストのセクション',
'rightSidebar.contextNotesTodo.sections.resize': 'セクションサイドバーの幅を変更',
'rightSidebar.contextNotesTodo.memory.scope.project': 'プロジェクト',
'rightSidebar.contextNotesTodo.memory.scope.label': 'メモリの範囲',
'rightSidebar.contextNotesTodo.memory.scope.global': 'あなたについて',
'rightSidebar.contextNotesTodo.memory.type.fact': '事実',
'rightSidebar.contextNotesTodo.memory.badge.new': '新規',
'rightSidebar.contextNotesTodo.memory.flagged': 'エージェントには渡されません — 指示のように読めます',
'rightSidebar.contextNotesTodo.memory.badge.changed': '変更',
'rightSidebar.contextNotesTodo.memory.type.preference': '設定',
'rightSidebar.contextNotesTodo.memory.type.reference': '参照',
'rightSidebar.contextNotesTodo.memory.actions.delete': 'この項目を削除',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': 'メモリのタイトル',
'rightSidebar.contextNotesTodo.memory.actions.editBody': 'メモリの本文',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': 'メモリを保存できませんでした',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': '項目を削除できませんでした',
'rightSidebar.contextNotesTodo.memory.empty.nothing': 'エージェントはまだ何も保存していません。',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': '検索条件に一致する項目はありません。',
'rightSidebar.contextNotesTodo.memory.empty.noProject': 'プロジェクトを開くと、エージェントが記憶している内容を確認できます。',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': '保存された記憶を読み込めませんでした。失われてはいません。もう一度お試しください。',
'rightSidebar.contextNotesTodo.todo.clearCompleted': '完了をクリア',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'TODOを追加',
'rightSidebar.contextNotesTodo.todo.addAria': 'TODOを追加',
@@ -1550,13 +1598,9 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.todo.actions.delete': '「{text}」を削除',
'rightSidebar.contextNotesTodo.todo.actions.send': '「{text}」を送信',
'rightSidebar.contextNotesTodo.todo.actions.reorder': '「{text}」を並び替え',
'rightSidebar.contextNotesTodo.todo.resizeAria': 'TODOリストのサイズを変更',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '現在のセッションに送信',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '新しいセッションに送信',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '新しいワークツリーセッションに送信',
'rightSidebar.contextNotesTodo.plans.title': '計画',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count}ファイル',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count}ファイル',
'rightSidebar.contextNotesTodo.plans.importFromFile': 'ファイルから計画をインポート',
'rightSidebar.contextNotesTodo.plans.empty': 'まだ保存された計画はありません。',
'rightSidebar.contextNotesTodo.plans.deletePlan': '計画を削除',
@@ -1576,6 +1620,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': 'TODOを新しいセッションに送信しました',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': 'TODOを新しいワークツリーセッションに送信しました',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': 'TODOの送信に失敗しました',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': '計画を更新できませんでした',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': '計画の削除に失敗しました',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': '計画ファイルが空です',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': '計画のインポートに失敗しました',
@@ -2011,6 +2056,9 @@ export const dict: Record<I18nKey, string> = {
'chat.revert.toast.undo': '{preview}に元に戻しました',
'chat.revert.toast.redo': 'やり直しました',
'chat.revert.toast.restored': 'すべてのメッセージを復元しました',
'chat.toast.opencodeRestartInterrupted.title': 'チャットが中断されました',
'chat.toast.opencodeRestartInterrupted.description': '応答の生成中に OpenCode が再起動しました。続行するにはメッセージを送信してください。',
'chat.toast.opencodeRestartInterrupted.openSession': 'セッションを開く',
'chat.errorBoundary.title': 'チャットエラー',
'chat.errorBoundary.description': 'チャットインターフェースでエラーが発生しました。一時的なネットワーク問題または破損したメッセージデータが原因の可能性があります。',
'chat.errorBoundary.sessionLabel': 'セッション',
@@ -2978,13 +3026,15 @@ export const dict: Record<I18nKey, string> = {
'contextFileOpen.failure.unreadable': 'ファイルを開けませんでした',
'quota.window.5h': '5時間',
'quota.window.7d': '7日間制限',
'quota.window.7dSonnet': '7日間Sonnet制限',
'quota.window.7dOpus': '7日間Opus制限',
'quota.window.weekly': '週間制限',
'quota.window.extraUsage': '追加利用',
'quota.window.weekly': '毎週',
'quota.window.daily': '日次',
'quota.window.monthly': '月間制限',
'quota.window.monthly': '月',
'quota.window.credits': 'クレジット',
'quota.window.creditsBalance': 'クレジット残高',
'quota.window.monthlyCredits': '月間クレジット',
'quota.window.purchasedCredits': '購入済みクレジット',
'quota.window.freeCredits': '無料クレジット',
'quota.window.billingCycle': '請求サイクル',
'quota.window.auto': '自動',
'quota.window.api': 'API',
@@ -3031,6 +3081,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': '質問があります',
'chat.workStatus.section.contextBreakdown': 'コンテキストソース',
'chat.workStatus.breakdown.skills': 'スキル',
'chat.workStatus.breakdown.pinnedNote': 'メモ',
'chat.workStatus.breakdown.unpin': 'コンテキストからピンを外す',
'chat.workStatus.breakdown.pinnedPlan': 'プラン',
'chat.workStatus.breakdown.memory': 'エージェントメモリ',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} 件ピン留め',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} 件ピン留め',
'chat.workStatus.breakdown.mcp': 'MCP サーバー',
'chat.workStatus.action.openChanges': '変更を開く',
'chat.workStatus.action.openGit': 'Git パネルを開く',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 사용량 추적',
'settings.providers.page.openCodeGo.description': 'OpenCode Go 대시보드를 연결하여 롤링, 주간 및 월간 할당량을 표시합니다.',
@@ -864,16 +865,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': '수동',
'settings.skills.catalog.page.mode.external': 'External',
'settings.skills.catalog.page.title': '스킬 카탈로그',
'settings.skills.catalog.page.subtitle': '선별된 저장소에서 바로 사용 가능한 스킬을 설치하거나 직접 소스를 추가하세요.',
'settings.skills.catalog.page.section.sources': '소스',
'settings.skills.catalog.page.searchAllPlaceholder': '모든 소스에서 스킬 검색…',
'settings.skills.catalog.page.search.clear': '검색 지우기',
'settings.skills.catalog.page.source.skillsCount': '스킬: {count}개',
'settings.skills.catalog.page.source.stars': '스타: {count}',
'settings.skills.catalog.page.source.updated': '업데이트: {time}',
'settings.skills.catalog.page.source.addOwnTitle': '직접 소스 추가',
'settings.skills.catalog.page.source.addOwnDescription': '스킬이 있는 아무 Git 저장소',
'settings.skills.catalog.page.source.viewRepo': 'GitHub에서 저장소 열기',
'settings.skills.catalog.page.skill.viewOnGithub': 'GitHub에서 스킬 보기',
'settings.skills.catalog.page.list.searchTitle': '검색 결과',
'settings.skills.catalog.page.section.sourceRepository': '카탈로그 저장소',
'settings.skills.catalog.page.field.selectSourcePlaceholder': '저장소 선택',
'settings.skills.catalog.page.actions.refreshTitle': '새로고침',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'Catalog 제거',
'settings.skills.catalog.page.actions.addCatalog': 'Catalog 추가',
'settings.skills.catalog.page.actions.removeCatalog': 'Catalog 제거',
'settings.skills.catalog.page.actions.loadMoreSkills': '스킬 더 불러오기',
'settings.skills.catalog.page.loading.catalog': '로딩 중...',
'settings.skills.catalog.page.loading.skills': '스킬 불러오는 중...',
'settings.skills.catalog.page.loading.more': '로딩 중...',
'settings.skills.catalog.page.foundCount': '스킬 {count}개 발견',
'settings.skills.catalog.page.error.catalogTitle': 'Catalog 오류',
'settings.skills.catalog.page.empty.noSkillsTitle': '스킬을 찾을 수 없습니다',
@@ -881,7 +892,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': '설치됨({scope})',
'settings.skills.catalog.page.badge.notInstallable': '설치할 수 없음',
'settings.skills.catalog.page.badge.unknown': '알 수 없음',
'settings.skills.catalog.page.byOwnerPrefix': '작성자',
'settings.skills.catalog.page.removeDialog.title': 'Catalog 제거',
'settings.skills.catalog.page.removeDialog.description': '이 카탈로그를 제거하시겠습니까?',
'settings.openchamber.passkeys.title': 'Passkeys',
@@ -980,6 +990,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber Web 도구',
'settings.openchamber.tools.field.agentWebToolAria': 'OpenChamber Web 도구 활성화',
'settings.openchamber.tools.field.agentWebToolInfo': '에이전트가 OpenChamber 브라우저 패널에서 페이지를 확인하고 조작할 수 있습니다. URL 열기, 내용 읽기, 클릭, 입력, 스크롤, 모바일과 데스크톱 레이아웃 전환이 가능합니다. 각 세션에 작은 도구 설명이 추가됩니다. OpenCode를 다시 시작하면 적용됩니다.',
'settings.openchamber.tools.field.agentMemoryTool': '에이전트 메모리 도구',
'settings.openchamber.tools.field.agentMemoryToolAria': '에이전트 메모리 도구',
'settings.openchamber.tools.field.agentMemoryToolInfo': '에이전트가 배운 내용을 세션 간에 유지하도록 합니다. 저장소는 두 개로, 사용자에 대한 사실과 각 프로젝트에 대한 사실입니다. 세션에는 저장된 제목이 전달되어 관련 항목을 에이전트가 읽을 수 있습니다. 끄면 도구와 메모리 탭, 세션 색인이 모두 사라집니다. OpenCode를 다시 시작한 뒤 적용됩니다.',
'settings.openchamber.opencodeCli.tooltipPrefix': '선택적 절대 경로:',
'settings.openchamber.opencodeCli.tooltipSuffix': 'binary.',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode binary 경로',
@@ -1160,6 +1173,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': '{provider} 사용량',
'settings.usage.page.header.refreshing': '사용량 새로고침 중...',
'settings.usage.page.header.lastUpdated': '마지막 업데이트: {time}',
'settings.usage.page.header.lastUpdatedWithPlan': '플랜: {plan} · 마지막 업데이트: {time}',
'settings.usage.page.options.showInWorkStatusAria': '작업 상태 패널에 표시',
'settings.usage.page.options.showInWorkStatus': '작업 상태 패널에 표시',
'settings.usage.page.options.showInWorkStatusTooltip': '활성화하면 이 제공업체의 사용량이 작업 상태 패널에 표시됩니다.',
@@ -1326,13 +1340,18 @@ export const settingsDict = {
'settings.providers.page.custom.title': '사용자 정의 제공자',
'settings.providers.page.custom.editTitle': '사용자 지정 공급자 편집',
'settings.providers.page.custom.description': '기본 URL, 자격 증명, 모델 목록으로 OpenAI 호환 제공자를 추가합니다. OpenCode 설정에 저장되어 다른 제공자와 같이 채팅에서 사용할 수 있습니다.',
'settings.providers.page.custom.description': '기본 URL, 자격 증명, 모델 목록 및 지원되는 API 프로토콜로 제공자를 추가합니다. 채팅에 사용할 수 있도록 OpenCode 설정에 저장됩니다.',
'settings.providers.page.custom.field.providerID.label': '제공자 ID',
'settings.providers.page.custom.field.providerID.placeholder': 'my-provider',
'settings.providers.page.custom.field.providerID.info': '소문자, 숫자, 하이픈, 밑줄. OpenCode 제공자 ID로 사용됩니다.',
'settings.providers.page.custom.field.name.label': '표시 이름',
'settings.providers.page.custom.field.name.placeholder': '내 제공자',
'settings.providers.page.custom.field.name.info': '제공자 및 모델 선택기에 표시됩니다.',
'settings.providers.page.custom.field.protocol.label': 'API 프로토콜',
'settings.providers.page.custom.field.protocol.info': '이 API가 구현하는 요청 형식을 선택하세요.',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': '기본 URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'OpenAI 호환 API 기본 URL. http:// 또는 https://로 시작해야 합니다.',
@@ -2133,4 +2152,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n.ko,
} as const;
+70 -14
View File
@@ -998,6 +998,8 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.draft': '드래프트',
'gitView.pr.field.title': '제목',
'gitView.pr.githubNotConnected': 'GitHub에 연결되지 않음',
'gitView.pr.history.merged': 'PR #{number}이(가) {base}에 병합되었습니다.',
'gitView.pr.history.closed': 'PR #{number}이(가) 닫혔습니다.',
'gitView.pr.loadingDescription': '설명 로드 중…',
'gitView.pr.mergeMethod.merge': '병합 커밋 생성',
'gitView.pr.mergeMethod.rebase': '리베이스 후 병합',
@@ -1187,12 +1189,12 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '스몰 모델은 워크스루에 필요한 구조화된 응답을 지원하지 않습니다.',
'contextRail.surface.plan.description': '현재 계획 보기',
'contextRail.surface.pr.description': '현재 브랜치의 풀 리퀘스트를 생성, 검토, 병합',
'contextRail.surface.notes.description': '프로젝트의 노트, 할 일, 계획',
'contextRail.surface.notes.description': '프로젝트의 노트, 할 일, 계획, 에이전트 메모리',
'contextRail.surface.context.description': '세션 컨텍스트 및 토큰 사용량',
'contextRail.surface.browser.description': '내장 웹 브라우저',
'contextRail.surface.preview.description': '개발 서버 미리보기',
'contextRail.surface.chat.description': '나란히 연 세션',
'contextRail.surface.notes': '프로젝트 노트',
'contextRail.surface.notes': '프로젝트 지식',
'contextRail.editorTree.toggle': '파일 트리 표시 전환',
'contextPanel.browser.open': '브라우저 패널 열기',
'contextPanel.browser.addressAria': '브라우저 주소',
@@ -1322,6 +1324,14 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.toast.writeNotSupported': '쓰기를 지원하지 않음',
'sidebarFilesTree.toast.fileCreated': '파일 생성됨',
'sidebarFilesTree.toast.operationFailed': '작업 실패',
'sidebarFilesTree.toast.uploaded': '파일을 업로드했습니다',
'sidebarFilesTree.toast.uploadedWithoutConflicts': '충돌하지 않은 파일을 업로드했습니다',
'sidebarFilesTree.toast.uploadFailed': '일부 파일을 업로드하지 못했습니다',
'sidebarFilesTree.drop.target': '{path}에 업로드',
'sidebarFilesTree.drop.uploading': '{path}에 파일 업로드 중',
'sidebarFilesTree.dialog.uploadConflicts.title': '기존 파일을 교체할까요?',
'sidebarFilesTree.dialog.uploadConflicts.description': '같은 이름의 파일이 {path}에 이미 있습니다. 교체 작업은 취소할 수 없습니다.',
'sidebarFilesTree.dialog.uploadConflicts.replace': '교체',
'sidebarFilesTree.toast.folderNameRequired': '폴더 이름 필수',
'sidebarFilesTree.toast.folderCreated': '폴더 생성됨',
'sidebarFilesTree.toast.nameRequired': '이름 필수',
@@ -1429,10 +1439,12 @@ export const dict: Record<I18nKey, string> = {
'contextUsage.mobile.usedTokens': '사용한 토큰',
'contextUsage.mobile.contextLimit': '컨텍스트 한도',
'contextUsage.mobile.outputLimit': '출력 한도',
'contextUsage.mobile.cost': '비용',
'contextUsage.mobile.usage': '사용량',
'contextUsage.tooltip.usedTokens': '사용됨 토큰: {tokens}',
'contextUsage.tooltip.contextLimit': '컨텍스트 한도: {tokens}',
'contextUsage.tooltip.outputLimit': '출력 한도: {tokens}',
'contextUsage.tooltip.cost': '비용: {cost}',
'contextSidebar.session.untitled': '제목 없는 세션',
'contextSidebar.empty.openSession': '컨텍스트를 볼 세션을 여세요.',
'contextSidebar.section.context': '컨텍스트',
@@ -1459,6 +1471,7 @@ export const dict: Record<I18nKey, string> = {
'planView.file.defaultName': 'plan',
'planView.title.default': '플랜',
'planView.error.saveFailed': '저장 실패',
'planView.error.loadFailed': '이 계획을 불러오지 못했습니다',
'planView.error.previewUnavailable': '미리보기를 사용할 수 없음',
'planView.error.switchToEditMode': '문제를 수정하려면 편집 모드로 전환하세요.',
'planView.error.writeFailed': '쓰기 실패',
@@ -1541,11 +1554,46 @@ export const dict: Record<I18nKey, string> = {
'diffView.hunk.unsupported': '개별 허크 스테이징은 이 환경에서 지원되지 않습니다.',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '플랜',
'rightSidebar.contextNotesTodo.empty.selectProject': '메모와 Todo를 추가할 프로젝트를 선택하세요.',
'rightSidebar.contextNotesTodo.notes.title': '빠른 메모 - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': '컨텍스트, 리마인더, 링크를 기록하세요',
'rightSidebar.contextNotesTodo.todo.title': 'Todo',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count}개 항목',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count}개 항목',
'rightSidebar.contextNotesTodo.notes.addAria': '노트 추가',
'rightSidebar.contextNotesTodo.notes.empty': '아직 노트가 없습니다. 맥락이나 메모, 링크를 남겨 보세요.',
'rightSidebar.contextNotesTodo.notes.actions.expand': '노트 펼치기',
'rightSidebar.contextNotesTodo.notes.actions.collapse': '노트 접기',
'rightSidebar.contextNotesTodo.notes.actions.delete': '노트 삭제',
'rightSidebar.contextNotesTodo.notes.actions.pin': '에이전트 컨텍스트에 고정',
'rightSidebar.contextNotesTodo.notes.actions.unpin': '에이전트 컨텍스트에서 고정 해제',
'rightSidebar.contextNotesTodo.notes.source.selection': '채팅에서',
'rightSidebar.contextNotesTodo.notes.source.agent': '에이전트에서',
'rightSidebar.contextNotesTodo.search.placeholder': '검색',
'rightSidebar.contextNotesTodo.search.clear': '검색 지우기',
'rightSidebar.contextNotesTodo.search.noResults': '"{query}"과(와) 일치하는 항목이 없습니다.',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': '노트를 삭제하지 못했습니다',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': '노트를 만들지 못했습니다',
'rightSidebar.contextNotesTodo.tabs.notes': '노트',
'rightSidebar.contextNotesTodo.tabs.todos': '할 일',
'rightSidebar.contextNotesTodo.tabs.plans': '계획',
'rightSidebar.contextNotesTodo.plans.actions.back': '계획 목록으로',
'rightSidebar.contextNotesTodo.tabs.memory': '메모리',
'rightSidebar.contextNotesTodo.sections.label': '프로젝트 컨텍스트 섹션',
'rightSidebar.contextNotesTodo.sections.resize': '섹션 사이드바 너비 조절',
'rightSidebar.contextNotesTodo.memory.scope.project': '프로젝트',
'rightSidebar.contextNotesTodo.memory.scope.label': '메모리 범위',
'rightSidebar.contextNotesTodo.memory.scope.global': '사용자 정보',
'rightSidebar.contextNotesTodo.memory.type.fact': '사실',
'rightSidebar.contextNotesTodo.memory.badge.new': '신규',
'rightSidebar.contextNotesTodo.memory.flagged': '에이전트에 전달되지 않음 — 지시문처럼 읽힘',
'rightSidebar.contextNotesTodo.memory.badge.changed': '변경',
'rightSidebar.contextNotesTodo.memory.type.preference': '선호',
'rightSidebar.contextNotesTodo.memory.type.reference': '참조',
'rightSidebar.contextNotesTodo.memory.actions.delete': '이 항목 삭제',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': '메모리 제목',
'rightSidebar.contextNotesTodo.memory.actions.editBody': '메모리 내용',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': '메모리를 저장하지 못했습니다',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': '항목을 삭제하지 못했습니다',
'rightSidebar.contextNotesTodo.memory.empty.nothing': '에이전트가 아직 저장한 항목이 없습니다.',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': '검색과 일치하는 저장 항목이 없습니다.',
'rightSidebar.contextNotesTodo.memory.empty.noProject': '프로젝트를 열면 에이전트가 기억하는 내용을 볼 수 있습니다.',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': '저장된 메모리를 불러오지 못했습니다. 사라진 것은 없습니다. 다시 시도하세요.',
'rightSidebar.contextNotesTodo.todo.clearCompleted': '완료 항목 지우기',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'Todo 추가',
'rightSidebar.contextNotesTodo.todo.addAria': 'Todo 추가',
@@ -1556,13 +1604,9 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.todo.actions.delete': '"{text}" 삭제',
'rightSidebar.contextNotesTodo.todo.actions.send': '보내기 "{text}"',
'rightSidebar.contextNotesTodo.todo.actions.reorder': '재정렬 "{text}"',
'rightSidebar.contextNotesTodo.todo.resizeAria': '할 일 목록 크기 조정',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '현재 세션으로 보내기',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '새 세션으로 보내기',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '새 워크트리 세션으로 보내기',
'rightSidebar.contextNotesTodo.plans.title': '플랜',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} 파일',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} 파일',
'rightSidebar.contextNotesTodo.plans.importFromFile': '파일에서 플랜 가져오기',
'rightSidebar.contextNotesTodo.plans.empty': '아직 저장된 플랜 없음',
'rightSidebar.contextNotesTodo.plans.deletePlan': '플랜 삭제',
@@ -1582,6 +1626,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': '할 일을 새 세션으로 보냈습니다',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': '할 일을 새 워크트리 세션으로 보냈습니다',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': 'Todo 전송 실패',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': '계획을 업데이트하지 못했습니다',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': '플랜 삭제 실패',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': '플랜 파일이 비어 있음',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': '플랜 가져오기 실패',
@@ -2017,6 +2062,9 @@ export const dict: Record<I18nKey, string> = {
'chat.revert.toast.undo': '{preview}(으)로 되돌림',
'chat.revert.toast.redo': '다시 실행',
'chat.revert.toast.restored': '모든 메시지 복원됨',
'chat.toast.opencodeRestartInterrupted.title': '채팅이 중단되었습니다',
'chat.toast.opencodeRestartInterrupted.description': '응답이 진행 중인 동안 OpenCode가 다시 시작되었습니다. 계속하려면 메시지를 보내세요.',
'chat.toast.opencodeRestartInterrupted.openSession': '세션 열기',
'chat.errorBoundary.title': '채팅 오류',
'chat.errorBoundary.description': '채팅 인터페이스에서 오류가 발생했습니다. 일시적인 네트워크 이슈 또는 손상된 메시지 데이터 때문일 수 있습니다.',
'chat.errorBoundary.sessionLabel': '세션',
@@ -2982,13 +3030,15 @@ export const dict: Record<I18nKey, string> = {
'contextFileOpen.failure.unreadable': 'Failed to open file',
'quota.window.5h': '5-Hour',
'quota.window.7d': '7-Day Limit',
'quota.window.7dSonnet': '7-Day Sonnet Limit',
'quota.window.7dOpus': '7-Day Opus Limit',
'quota.window.weekly': 'Weekly Limit',
'quota.window.extraUsage': '추가 사용량',
'quota.window.weekly': '매주',
'quota.window.daily': 'Daily',
'quota.window.monthly': 'Monthly Limit',
'quota.window.monthly': '매월',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Credits Balance',
'quota.window.monthlyCredits': '월간 크레딧',
'quota.window.purchasedCredits': '구매한 크레딧',
'quota.window.freeCredits': '무료 크레딧',
'quota.window.billingCycle': 'Billing Cycle',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3031,6 +3081,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': '질문함',
'chat.workStatus.section.contextBreakdown': '컨텍스트 소스',
'chat.workStatus.breakdown.skills': '스킬',
'chat.workStatus.breakdown.pinnedNote': '노트',
'chat.workStatus.breakdown.unpin': '컨텍스트에서 고정 해제',
'chat.workStatus.breakdown.pinnedPlan': '계획',
'chat.workStatus.breakdown.memory': '에이전트 메모리',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count}개 고정',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count}개 고정',
'chat.workStatus.breakdown.mcp': 'MCP 서버',
'chat.workStatus.action.openChanges': '변경 사항 열기',
'chat.workStatus.action.openGit': 'Git 패널 열기',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Śledzenie użycia OpenCode Go',
'settings.providers.page.openCodeGo.description': 'Połącz panel OpenCode Go, aby wyświetlać limity kroczące, tygodniowe i miesięczne.',
@@ -864,6 +865,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'Narzędzie OpenChamber Web',
'settings.openchamber.tools.field.agentWebToolAria': 'Włącz narzędzie OpenChamber Web',
'settings.openchamber.tools.field.agentWebToolInfo': 'Pozwól agentom oglądać stronę w panelu przeglądarki OpenChamber i wchodzić z nią w interakcję: otwierać adres URL, czytać treść, klikać, pisać, przewijać i przełączać między układem mobilnym a desktopowym. Dodaje krótki opis narzędzia do każdej sesji. Zastosowane po ponownym uruchomieniu OpenCode.',
'settings.openchamber.tools.field.agentMemoryTool': 'Narzędzie pamięci agenta',
'settings.openchamber.tools.field.agentMemoryToolAria': 'Narzędzie pamięci agenta',
'settings.openchamber.tools.field.agentMemoryToolInfo': 'Pozwala agentom zachowywać to, czego się nauczyły, pomiędzy sesjami, w dwóch magazynach: co jest prawdą o Tobie i co jest prawdą o danym projekcie. Sesje otrzymują zapisane tytuły, aby agent mógł odczytać wpis, gdy jest istotny. Wyłączenie usuwa narzędzie, kartę Pamięć i indeks sesji. Działa po ponownym uruchomieniu OpenCode.',
'settings.openchamber.opencodeCli.tooltipPrefix': 'Opcjonalna ścieżka absolutna do',
'settings.openchamber.opencodeCli.tooltipSuffix': 'pliku binarnego.',
'settings.openchamber.passkeys.actions.add': 'Dodaj klucz dostępu (passkey)',
@@ -1415,13 +1419,18 @@ export const settingsDict = {
'settings.providers.page.custom.title': 'Niestandardowy dostawca',
'settings.providers.page.custom.editTitle': 'Edytuj niestandardowego dostawcę',
'settings.providers.page.custom.description': 'Dodaj dostawcę zgodnego z OpenAI, podając adres bazowy, poświadczenia i listę modeli. Zapisuje się w konfiguracji OpenCode i działa w czacie jak każdy inny dostawca.',
'settings.providers.page.custom.description': 'Dodaj dostawcę z adresem bazowym, poświadczeniami, listą modeli i obsługiwanym protokołem API. Zapisuje się w konfiguracji OpenCode do użycia w czacie.',
'settings.providers.page.custom.field.providerID.label': 'ID dostawcy',
'settings.providers.page.custom.field.providerID.placeholder': 'moj-dostawca',
'settings.providers.page.custom.field.providerID.info': 'Małe litery, cyfry, myślniki i podkreślenia. Używane jako ID dostawcy OpenCode.',
'settings.providers.page.custom.field.name.label': 'Nazwa wyświetlana',
'settings.providers.page.custom.field.name.placeholder': 'Mój dostawca',
'settings.providers.page.custom.field.name.info': 'Widoczna w selektorach dostawcy i modelu.',
'settings.providers.page.custom.field.protocol.label': 'Protokół API',
'settings.providers.page.custom.field.protocol.info': 'Wybierz format żądania obsługiwany przez to API.',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': 'Adres bazowy',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': 'Bazowy URL API zgodnego z OpenAI. Musi zaczynać się od http:// lub https://.',
@@ -1821,21 +1830,18 @@ export const settingsDict = {
'settings.skills.catalog.installSkill.toast.installFailed': 'Nie udało się zainstalować umiejętności',
'settings.skills.catalog.installSkill.toast.installed': 'Umiejętność została zainstalowana',
'settings.skills.catalog.page.actions.addCatalog': 'Dodaj katalog',
'settings.skills.catalog.page.actions.loadMoreSkills': 'Załaduj więcej umiejętności',
'settings.skills.catalog.page.actions.refreshTitle': 'Odśwież',
'settings.skills.catalog.page.actions.removeCatalog': 'Usuń katalog',
'settings.skills.catalog.page.actions.removeCatalogTitle': 'Usuń katalog',
'settings.skills.catalog.page.badge.installed': 'zainstalowano ({scope})',
'settings.skills.catalog.page.badge.notInstallable': 'nie można zainstalować',
'settings.skills.catalog.page.badge.unknown': 'nieznane',
'settings.skills.catalog.page.byOwnerPrefix': 'autor:',
'settings.skills.catalog.page.empty.noSkillsDescription': 'Spróbuj innego wyszukiwania lub odśwież katalog',
'settings.skills.catalog.page.empty.noSkillsTitle': 'Nie znaleziono umiejętności',
'settings.skills.catalog.page.error.catalogTitle': 'Błąd katalogu',
'settings.skills.catalog.page.field.selectSourcePlaceholder': 'Wybierz źródło',
'settings.skills.catalog.page.foundCount': 'Znaleziono {count} umiejętności',
'settings.skills.catalog.page.loading.catalog': 'Ładowanie...',
'settings.skills.catalog.page.loading.more': 'Ładowanie...',
'settings.skills.catalog.page.loading.skills': 'Ładowanie umiejętności...',
'settings.skills.catalog.page.mode.external': 'Zewnętrzny',
'settings.skills.catalog.page.mode.manual': 'Ręczny',
@@ -1843,6 +1849,18 @@ export const settingsDict = {
'settings.skills.catalog.page.removeDialog.title': 'Usuń katalog',
'settings.skills.catalog.page.section.sourceRepository': 'Repozytorium źródłowe',
'settings.skills.catalog.page.title': 'Katalog umiejętności',
'settings.skills.catalog.page.subtitle': 'Instaluj gotowe umiejętności z kuratorowanych repozytoriów lub dodaj własne źródło.',
'settings.skills.catalog.page.section.sources': 'Źródła',
'settings.skills.catalog.page.searchAllPlaceholder': 'Szukaj umiejętności we wszystkich źródłach…',
'settings.skills.catalog.page.search.clear': 'Wyczyść wyszukiwanie',
'settings.skills.catalog.page.source.skillsCount': 'Umiejętności: {count}',
'settings.skills.catalog.page.source.stars': 'Gwiazdki: {count}',
'settings.skills.catalog.page.source.updated': 'Zaktualizowano {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Dodaj własne źródło',
'settings.skills.catalog.page.source.addOwnDescription': 'Dowolne repozytorium Git z umiejętnościami',
'settings.skills.catalog.page.source.viewRepo': 'Otwórz repozytorium na GitHubie',
'settings.skills.catalog.page.skill.viewOnGithub': 'Zobacz umiejętność na GitHubie',
'settings.skills.catalog.page.list.searchTitle': 'Wyniki wyszukiwania',
'settings.skills.catalog.shared.actions.install': 'Zainstaluj',
'settings.skills.catalog.shared.actions.installing': 'Instalowanie...',
'settings.skills.catalog.shared.actions.scan': 'Skanuj',
@@ -1962,6 +1980,7 @@ export const settingsDict = {
'settings.usage.pace.waitSeparator': ' · Czekaj ',
'settings.usage.page.empty.selectProvider': 'Wybierz dostawcę, aby wyświetlić szczegóły użycia.',
'settings.usage.page.header.lastUpdated': 'Ostatnio aktualizowano: {time}',
'settings.usage.page.header.lastUpdatedWithPlan': 'Plan: {plan} · Ostatnia aktualizacja: {time}',
'settings.usage.page.header.providerUsage': 'Użycie {provider}',
'settings.usage.page.header.refreshing': 'Odświeżanie użycia...',
'settings.usage.page.options.showInWorkStatus': 'Pokaż w panelu statusu pracy',
@@ -2125,4 +2144,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n.pl,
};
+70 -14
View File
@@ -766,6 +766,9 @@ export const dict: Record<I18nKey, string> = {
'chat.revert.toast.undo': 'Cofnięte do {preview}',
'chat.revert.toast.redo': 'Ponowione',
'chat.revert.toast.restored': 'Przywrócono wszystkie wiadomości',
'chat.toast.opencodeRestartInterrupted.title': 'Czat został przerwany',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode uruchomił się ponownie podczas generowania odpowiedzi. Wyślij wiadomość, aby kontynuować.',
'chat.toast.opencodeRestartInterrupted.openSession': 'Otwórz sesję',
'chat.errorBoundary.title': 'Błąd Czatu',
'chat.errorBoundary.description': 'Interfejs czatu napotkał błąd. Może to być spowodowane tymczasowym problemem sieciowym lub uszkodzonymi danymi wiadomości.',
'chat.errorBoundary.sessionLabel': 'Sesja',
@@ -1501,12 +1504,12 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Mały model nie obsługuje ustrukturyzowanych odpowiedzi wymaganych przez przewodnik.',
'contextRail.surface.plan.description': 'Zobacz bieżący plan',
'contextRail.surface.pr.description': 'Twórz, przeglądaj i scalaj pull request bieżącej gałęzi',
'contextRail.surface.notes.description': 'Notatki, zadania i plany projektu',
'contextRail.surface.notes.description': 'Notatki, zadania, plany i pamięć agenta dla projektu',
'contextRail.surface.context.description': 'Kontekst sesji i zużycie tokenów',
'contextRail.surface.browser.description': 'Wbudowana przeglądarka',
'contextRail.surface.preview.description': 'Podgląd serwera deweloperskiego',
'contextRail.surface.chat.description': 'Sesja otwarta obok',
'contextRail.surface.notes': 'Notatki projektu',
'contextRail.surface.notes': 'Wiedza o projekcie',
'contextRail.editorTree.toggle': 'Przełącz drzewo plików',
'contextPanel.browser.open': 'Otwórz panel przeglądarki',
'contextPanel.browser.addressAria': 'Adres przeglądarki',
@@ -1639,11 +1642,13 @@ export const dict: Record<I18nKey, string> = {
'contextUsage.aria.label': 'Użycie kontekstu',
'contextUsage.mobile.contextLimit': 'Limit kontekstu',
'contextUsage.mobile.outputLimit': 'Limit wyjścia',
'contextUsage.mobile.cost': 'Koszt',
'contextUsage.mobile.title': 'Użycie kontekstu',
'contextUsage.mobile.usage': 'Zużycie',
'contextUsage.mobile.usedTokens': 'Zużyte tokeny',
'contextUsage.tooltip.contextLimit': 'Limit kontekstu: {tokens}',
'contextUsage.tooltip.outputLimit': 'Limit wyjścia: {tokens}',
'contextUsage.tooltip.cost': 'Koszt: {cost}',
'contextUsage.tooltip.usedTokens': 'Zużyte tokeny: {tokens}',
'desktopHostSwitcher.actions.add': 'Dodaj',
'desktopHostSwitcher.actions.addInstance': 'Dodaj instancję',
@@ -2187,6 +2192,8 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.draft': 'Szkic',
'gitView.pr.field.title': 'Tytuł',
'gitView.pr.githubNotConnected': 'GitHub nie jest połączony',
'gitView.pr.history.merged': 'PR #{number} został scalony do {base}.',
'gitView.pr.history.closed': 'PR #{number} został zamknięty.',
'gitView.pr.loadingDescription': 'Loading description...',
'gitView.pr.mergeMethod.merge': 'Create a merge commit',
'gitView.pr.mergeMethod.rebase': 'Rebase and merge',
@@ -2529,6 +2536,7 @@ export const dict: Record<I18nKey, string> = {
'planView.actions.sendToNewWorktreeSession': 'Wyślij do nowej sesji drzewa pracy',
'planView.error.previewUnavailable': 'Podgląd jest niedostępny',
'planView.error.saveFailed': 'Nie udało się zapisać',
'planView.error.loadFailed': 'Nie udało się wczytać tego planu',
'planView.error.switchToEditMode': 'Przełącz do trybu edycji, aby naprawić problem.',
'planView.error.writeFailed': 'Nie udało się zapisać',
'planView.error.writePlanFileFailed': 'Nie udało się zapisać pliku planu ({status})',
@@ -2582,15 +2590,11 @@ export const dict: Record<I18nKey, string> = {
'projectEditDialog.toast.iconUpdated': 'Zaktualizowano ikonę projektu',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Wybierz projekt, aby dodać notatki i zadania.',
'rightSidebar.contextNotesTodo.notes.placeholder': 'Zapisz kontekst, przypomnienia lub linki',
'rightSidebar.contextNotesTodo.notes.title': 'Szybkie notatki — {project}',
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.plans.deletePlan': 'Usuń plan',
'rightSidebar.contextNotesTodo.plans.deletePlanWithTitle': 'Usuń plan „{title}”',
'rightSidebar.contextNotesTodo.plans.empty': 'Brak zapisanych planów.',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} plików',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} plik',
'rightSidebar.contextNotesTodo.plans.importFromFile': 'Importuj plan z pliku',
'rightSidebar.contextNotesTodo.plans.title': 'Plany',
'rightSidebar.contextNotesTodo.sendDialog.actions.cancel': 'Anuluj',
'rightSidebar.contextNotesTodo.sendDialog.actions.send': 'Wyślij',
'rightSidebar.contextNotesTodo.sendDialog.actions.sending': 'Wysyłanie',
@@ -2598,6 +2602,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.sendDialog.title.newWorktree': 'Wyślij do nowego drzewa pracy',
'rightSidebar.contextNotesTodo.sendDialog.variant.default': 'Domyślny',
'rightSidebar.contextNotesTodo.toast.createSessionFailed': 'Nie udało się utworzyć sesji',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': 'Nie udało się zaktualizować planu',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': 'Nie udało się usunąć planu',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': 'Nie udało się zaimportować planu',
'rightSidebar.contextNotesTodo.toast.loadNotesFailed': 'Nie udało się załadować notatek projektu',
@@ -2617,17 +2622,52 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.todo.actions.markComplete': 'Oznacz „{text}” jako ukończone',
'rightSidebar.contextNotesTodo.todo.actions.send': 'Wyślij „{text}”',
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Zmień kolejność "{text}"',
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Zmień rozmiar listy zadań',
'rightSidebar.contextNotesTodo.todo.addAria': 'Dodaj zadanie',
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Wyczyść ukończone',
'rightSidebar.contextNotesTodo.todo.empty': 'Brak zadań. Dodaj krótką checklistę dla tego projektu.',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': 'Dodaj zadanie',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count} elementów',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count} element',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'Wyślij do bieżącej sesji',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'Wyślij do nowej sesji',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'Wyślij do nowej sesji drzewa pracy',
'rightSidebar.contextNotesTodo.todo.title': 'Zadania',
'rightSidebar.contextNotesTodo.notes.addAria': 'Dodaj notatkę',
'rightSidebar.contextNotesTodo.notes.empty': 'Brak notatek. Zapisz kontekst, przypomnienia lub linki.',
'rightSidebar.contextNotesTodo.notes.actions.expand': 'Rozwiń notatkę',
'rightSidebar.contextNotesTodo.notes.actions.collapse': 'Zwiń notatkę',
'rightSidebar.contextNotesTodo.notes.actions.delete': 'Usuń notatkę',
'rightSidebar.contextNotesTodo.notes.actions.pin': 'Przypnij do kontekstu agenta',
'rightSidebar.contextNotesTodo.notes.actions.unpin': 'Odepnij od kontekstu agenta',
'rightSidebar.contextNotesTodo.notes.source.selection': 'Z czatu',
'rightSidebar.contextNotesTodo.notes.source.agent': 'Od agenta',
'rightSidebar.contextNotesTodo.search.placeholder': 'Szukaj',
'rightSidebar.contextNotesTodo.search.clear': 'Wyczyść wyszukiwanie',
'rightSidebar.contextNotesTodo.search.noResults': 'Nic nie pasuje do "{query}".',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': 'Nie udało się usunąć notatki',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': 'Nie udało się utworzyć notatki',
'rightSidebar.contextNotesTodo.tabs.notes': 'Notatki',
'rightSidebar.contextNotesTodo.tabs.todos': 'Zadania',
'rightSidebar.contextNotesTodo.tabs.plans': 'Plany',
'rightSidebar.contextNotesTodo.plans.actions.back': 'Wróć do planów',
'rightSidebar.contextNotesTodo.tabs.memory': 'Pamięć',
'rightSidebar.contextNotesTodo.sections.label': 'Sekcje kontekstu projektu',
'rightSidebar.contextNotesTodo.sections.resize': 'Zmień szerokość paska sekcji',
'rightSidebar.contextNotesTodo.memory.scope.project': 'Projekt',
'rightSidebar.contextNotesTodo.memory.scope.label': 'Zakres pamięci',
'rightSidebar.contextNotesTodo.memory.scope.global': 'O Tobie',
'rightSidebar.contextNotesTodo.memory.type.fact': 'fakt',
'rightSidebar.contextNotesTodo.memory.badge.new': 'nowe',
'rightSidebar.contextNotesTodo.memory.flagged': 'Wstrzymane — czyta się jak instrukcja',
'rightSidebar.contextNotesTodo.memory.badge.changed': 'zmienione',
'rightSidebar.contextNotesTodo.memory.type.preference': 'preferencja',
'rightSidebar.contextNotesTodo.memory.type.reference': 'odnośnik',
'rightSidebar.contextNotesTodo.memory.actions.delete': 'Zapomnij ten wpis',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': 'Tytuł wpisu',
'rightSidebar.contextNotesTodo.memory.actions.editBody': 'Treść wpisu',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': 'Nie udało się zapisać wpisu',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': 'Nie udało się zapomnieć wpisu',
'rightSidebar.contextNotesTodo.memory.empty.nothing': 'Agent nic tu jeszcze nie zapisał.',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': 'Żaden zapisany wpis nie pasuje do wyszukiwania.',
'rightSidebar.contextNotesTodo.memory.empty.noProject': 'Otwórz projekt, aby zobaczyć, co agent o nim pamięta.',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': 'Nie udało się wczytać zapisanej pamięci. Nic nie przepadło — spróbuj ponownie.',
'saveProjectPlanDialog.actions.cancel': 'Anuluj',
'saveProjectPlanDialog.actions.save': 'Zapisz',
'saveProjectPlanDialog.actions.saving': 'Saving...',
@@ -2810,6 +2850,14 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.toast.folderNameRequired': 'Nazwa folderu jest wymagana',
'sidebarFilesTree.toast.nameRequired': 'Nazwa jest wymagana',
'sidebarFilesTree.toast.operationFailed': 'Operacja nie powiodła się',
'sidebarFilesTree.toast.uploaded': 'Pliki przesłano',
'sidebarFilesTree.toast.uploadedWithoutConflicts': 'Przesłano pliki bez konfliktów',
'sidebarFilesTree.toast.uploadFailed': 'Nie udało się przesłać niektórych plików',
'sidebarFilesTree.drop.target': 'Prześlij do {path}',
'sidebarFilesTree.drop.uploading': 'Przesyłanie plików do {path}',
'sidebarFilesTree.dialog.uploadConflicts.title': 'Zastąpić istniejące pliki?',
'sidebarFilesTree.dialog.uploadConflicts.description': 'Pliki o tych nazwach już istnieją w {path}. Zastąpienia nie można cofnąć.',
'sidebarFilesTree.dialog.uploadConflicts.replace': 'Zastąp',
'sidebarFilesTree.toast.pathCopied': 'Ścieżka skopiowana',
'sidebarFilesTree.toast.renameNotSupported': 'Zmiana nazwy nie jest obsługiwana',
'sidebarFilesTree.toast.renamedSuccessfully': 'Zmieniono nazwę pomyślnie',
@@ -2999,13 +3047,15 @@ export const dict: Record<I18nKey, string> = {
'contextFileOpen.failure.unreadable': 'Failed to open file',
'quota.window.5h': '5-Hour',
'quota.window.7d': '7-Day Limit',
'quota.window.7dSonnet': '7-Day Sonnet Limit',
'quota.window.7dOpus': '7-Day Opus Limit',
'quota.window.weekly': 'Weekly Limit',
'quota.window.extraUsage': 'Dodatkowe zużycie',
'quota.window.weekly': 'Tygodniowo',
'quota.window.daily': 'Daily',
'quota.window.monthly': 'Monthly Limit',
'quota.window.monthly': 'Miesięcznie',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Credits Balance',
'quota.window.monthlyCredits': 'Kredyty miesięczne',
'quota.window.purchasedCredits': 'Kupione kredyty',
'quota.window.freeCredits': 'Darmowe kredyty',
'quota.window.billingCycle': 'Billing Cycle',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3048,6 +3098,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': 'zadał pytanie',
'chat.workStatus.section.contextBreakdown': 'Źródła kontekstu',
'chat.workStatus.breakdown.skills': 'Umiejętności',
'chat.workStatus.breakdown.pinnedNote': 'notatka',
'chat.workStatus.breakdown.unpin': 'Odepnij od kontekstu',
'chat.workStatus.breakdown.pinnedPlan': 'plan',
'chat.workStatus.breakdown.memory': 'Pamięć agenta',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} przypięte',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} przypiętych',
'chat.workStatus.breakdown.mcp': 'Serwery MCP',
'chat.workStatus.action.openChanges': 'Otwórz zmiany',
'chat.workStatus.action.openGit': 'Otwórz panel Git',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Monitoramento de uso do OpenCode Go',
'settings.providers.page.openCodeGo.description': 'Conecte o painel do OpenCode Go para exibir as cotas móvel, semanal e mensal.',
@@ -864,16 +865,26 @@ export const settingsDict = {
"settings.skills.catalog.page.mode.manual": "Manual",
"settings.skills.catalog.page.mode.external": "Externo",
"settings.skills.catalog.page.title": "Catálogo de habilidades",
'settings.skills.catalog.page.subtitle': 'Instale skills prontas de repositórios curados ou adicione sua própria fonte.',
'settings.skills.catalog.page.section.sources': 'Fontes',
'settings.skills.catalog.page.searchAllPlaceholder': 'Pesquisar skills em todas as fontes…',
'settings.skills.catalog.page.search.clear': 'Limpar pesquisa',
'settings.skills.catalog.page.source.skillsCount': 'Skills: {count}',
'settings.skills.catalog.page.source.stars': 'Estrelas: {count}',
'settings.skills.catalog.page.source.updated': 'Atualizado {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Adicionar sua própria fonte',
'settings.skills.catalog.page.source.addOwnDescription': 'Qualquer repositório Git com skills',
'settings.skills.catalog.page.source.viewRepo': 'Abrir repositório no GitHub',
'settings.skills.catalog.page.skill.viewOnGithub': 'Ver skill no GitHub',
'settings.skills.catalog.page.list.searchTitle': 'Resultados da pesquisa',
"settings.skills.catalog.page.section.sourceRepository": "Repositório de origem",
"settings.skills.catalog.page.field.selectSourcePlaceholder": "Selecionar origem",
"settings.skills.catalog.page.actions.refreshTitle": "Atualizar",
"settings.skills.catalog.page.actions.removeCatalogTitle": "Excluir catálogo",
"settings.skills.catalog.page.actions.addCatalog": "Adicionar catálogo",
"settings.skills.catalog.page.actions.removeCatalog": "Excluir catálogo",
"settings.skills.catalog.page.actions.loadMoreSkills": "Carregar mais habilidades",
"settings.skills.catalog.page.loading.catalog": "Carregando...",
"settings.skills.catalog.page.loading.skills": "Carregando habilidades...",
"settings.skills.catalog.page.loading.more": "Carregando...",
"settings.skills.catalog.page.foundCount": "{count} habilidade(es) encontrada(s)",
"settings.skills.catalog.page.error.catalogTitle": "Erro do catálogo",
"settings.skills.catalog.page.empty.noSkillsTitle": "Nenhuma habilidade encontrada",
@@ -881,7 +892,6 @@ export const settingsDict = {
"settings.skills.catalog.page.badge.installed": "instalado ({scope})",
"settings.skills.catalog.page.badge.notInstallable": "não instalável",
"settings.skills.catalog.page.badge.unknown": "desconhecido",
"settings.skills.catalog.page.byOwnerPrefix": "por",
"settings.skills.catalog.page.removeDialog.title": "Excluir catálogo",
"settings.skills.catalog.page.removeDialog.description": "Tem certeza de que deseja excluir este catálogo?",
"settings.openchamber.passkeys.title": "Chaves de acesso",
@@ -980,6 +990,9 @@ export const settingsDict = {
"settings.openchamber.tools.field.agentWebTool": "Ferramenta OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolAria": "Ativar a ferramenta OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolInfo": "Permita que agentes vejam a página no painel de navegador do OpenChamber e interajam com ela: abrir uma URL, ler o conteúdo, clicar, digitar, rolar e alternar entre layout móvel e desktop. Adiciona uma pequena descrição de ferramenta a cada sessão. Aplicado após reiniciar o OpenCode.",
"settings.openchamber.tools.field.agentMemoryTool": "Ferramenta de memória do agente",
"settings.openchamber.tools.field.agentMemoryToolAria": "Ferramenta de memória do agente",
"settings.openchamber.tools.field.agentMemoryToolInfo": "Permite que os agentes guardem o que aprendem entre sessões, em dois armazenamentos: o que é verdade sobre você e o que é verdade sobre cada projeto. As sessões recebem os títulos armazenados para que o agente possa ler uma entrada quando for relevante. Desativar remove a ferramenta, a aba Memória e o índice da sessão. Vale após reiniciar o OpenCode.",
"settings.openchamber.opencodeCli.tooltipPrefix": "Caminho absoluto opcional para o",
"settings.openchamber.opencodeCli.tooltipSuffix": "executável.",
"settings.openchamber.opencodeCli.field.binaryPath": "Caminho do executável do OpenCode",
@@ -1160,6 +1173,7 @@ export const settingsDict = {
"settings.usage.page.header.providerUsage": "Uso de {provider}",
"settings.usage.page.header.refreshing": "Atualizando uso...",
"settings.usage.page.header.lastUpdated": "Última atualização: {time}",
"settings.usage.page.header.lastUpdatedWithPlan": "Plano: {plan} · Última atualização: {time}",
"settings.usage.page.options.showInWorkStatusAria": "Mostrar no painel de status do trabalho",
"settings.usage.page.options.showInWorkStatus": "Mostrar no painel de status do trabalho",
"settings.usage.page.options.showInWorkStatusTooltip": "Quando habilitado, o uso deste provedor ficará visível no painel de status do trabalho.",
@@ -1326,13 +1340,18 @@ export const settingsDict = {
"settings.providers.page.custom.title": "Provedor personalizado",
"settings.providers.page.custom.editTitle": "Editar provedor personalizado",
"settings.providers.page.custom.description": "Adicione um provedor compatível com OpenAI com URL base, credenciais e lista de modelos. Salvo na configuração do OpenCode para uso no chat como qualquer outro provedor.",
"settings.providers.page.custom.description": "Adicione um provedor com URL base, credenciais, lista de modelos e protocolo de API compatível. Ele é salvo na configuração do OpenCode para uso no chat.",
"settings.providers.page.custom.field.providerID.label": "ID do provedor",
"settings.providers.page.custom.field.providerID.placeholder": "meu-provedor",
"settings.providers.page.custom.field.providerID.info": "Letras minúsculas, números, hífens e sublinhados. Usado como ID de provedor do OpenCode.",
"settings.providers.page.custom.field.name.label": "Nome de exibição",
"settings.providers.page.custom.field.name.placeholder": "Meu provedor",
"settings.providers.page.custom.field.name.info": "Mostrado nos seletores de provedor e modelo.",
"settings.providers.page.custom.field.protocol.label": "Protocolo de API",
"settings.providers.page.custom.field.protocol.info": "Escolha o formato de solicitação implementado por esta API.",
"settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions",
"settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses",
"settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages",
"settings.providers.page.custom.field.baseURL.label": "URL base",
"settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1",
"settings.providers.page.custom.field.baseURL.info": "URL base da API compatível com OpenAI. Deve começar com http:// ou https://.",
@@ -2133,4 +2152,5 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...thirdPartyIntegrationI18n['pt-BR'],
} as const;
+70 -14
View File
@@ -998,6 +998,8 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.draft": "Borrador",
"gitView.pr.field.title": "Título",
"gitView.pr.githubNotConnected": "GitHub não está conectado",
"gitView.pr.history.merged": "A PR #{number} foi mesclada em {base}.",
"gitView.pr.history.closed": "A PR #{number} foi fechada.",
"gitView.pr.loadingDescription": "Carregando descrição...",
"gitView.pr.mergeMethod.merge": "Criar um merge commit",
"gitView.pr.mergeMethod.rebase": "Rebase e merge",
@@ -1187,12 +1189,12 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "O modelo pequeno não suporta as respostas estruturadas que um percurso exige.",
"contextRail.surface.plan.description": "Ver o plano atual",
"contextRail.surface.pr.description": "Crie, revise e faça merge do pull request do branch atual",
"contextRail.surface.notes.description": "Notas, tarefas e planos do projeto",
"contextRail.surface.notes.description": "Notas, tarefas, planos e memória do agente do projeto",
"contextRail.surface.context.description": "Contexto da sessão e uso de tokens",
"contextRail.surface.browser.description": "Navegador web integrado",
"contextRail.surface.preview.description": "Pré-visualização do servidor de desenvolvimento",
"contextRail.surface.chat.description": "Sessão aberta lado a lado",
"contextRail.surface.notes": "Notas do projeto",
"contextRail.surface.notes": "Conhecimento do projeto",
"contextRail.editorTree.toggle": "Alternar árvore de arquivos",
"contextPanel.browser.open": "Abrir painel do navegador",
"contextPanel.browser.addressAria": "Endereço do navegador",
@@ -1286,6 +1288,14 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.toast.writeNotSupported": "A escrita não é compatível",
"sidebarFilesTree.toast.fileCreated": "Arquivo criado",
"sidebarFilesTree.toast.operationFailed": "Não foi possível completar a operación",
"sidebarFilesTree.toast.uploaded": "Arquivos enviados",
"sidebarFilesTree.toast.uploadedWithoutConflicts": "Os arquivos sem conflitos foram enviados",
"sidebarFilesTree.toast.uploadFailed": "Não foi possível enviar alguns arquivos",
"sidebarFilesTree.drop.target": "Enviar para {path}",
"sidebarFilesTree.drop.uploading": "Enviando arquivos para {path}",
"sidebarFilesTree.dialog.uploadConflicts.title": "Substituir arquivos existentes?",
"sidebarFilesTree.dialog.uploadConflicts.description": "Já existem arquivos com esses nomes em {path}. A substituição não pode ser desfeita.",
"sidebarFilesTree.dialog.uploadConflicts.replace": "Substituir",
"sidebarFilesTree.toast.folderNameRequired": "O nome de pasta é obrigatório",
"sidebarFilesTree.toast.folderCreated": "Pasta criada",
"sidebarFilesTree.toast.nameRequired": "O nome é obrigatório",
@@ -1393,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
"contextUsage.mobile.usedTokens": "Tokens usados",
"contextUsage.mobile.contextLimit": "Limite de contexto",
"contextUsage.mobile.outputLimit": "Limite de saída",
"contextUsage.mobile.cost": "Custo",
"contextUsage.mobile.usage": "Uso",
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
"contextUsage.tooltip.contextLimit": "Limite de contexto: {tokens}",
"contextUsage.tooltip.outputLimit": "Limite de saída: {tokens}",
"contextUsage.tooltip.cost": "Custo: {cost}",
"contextSidebar.session.untitled": "Sessão sem título",
"contextSidebar.empty.openSession": "Abrir uma sessão para inspecionar o contexto.",
"contextSidebar.section.context": "Contexto",
@@ -1423,6 +1435,7 @@ export const dict: Record<I18nKey, string> = {
"planView.file.defaultName": "plano",
"planView.title.default": "Plano",
"planView.error.saveFailed": "Não foi possível salvar",
"planView.error.loadFailed": "Não foi possível carregar este plano",
"planView.error.previewUnavailable": "Pré-visualização indisponível",
"planView.error.switchToEditMode": "Alterne para o modo de edição para resolver o problema.",
"planView.error.writeFailed": "Não foi possível gravar",
@@ -1517,11 +1530,46 @@ export const dict: Record<I18nKey, string> = {
"diffView.hunk.unsupported": "A preparação de trechos individuais não é suportada neste ambiente.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "Plano",
"rightSidebar.contextNotesTodo.empty.selectProject": "Selecione um projeto para adicionar notas e tarefas pendentes.",
"rightSidebar.contextNotesTodo.notes.title": "Notas rápidas — {project}",
"rightSidebar.contextNotesTodo.notes.placeholder": "Capture contexto, lembretes ou links",
"rightSidebar.contextNotesTodo.todo.title": "Tarefas pendentes",
"rightSidebar.contextNotesTodo.todo.itemsSingle": "{count} elemento",
"rightSidebar.contextNotesTodo.todo.itemsPlural": "{count} elementos",
"rightSidebar.contextNotesTodo.notes.addAria": "Adicionar nota",
"rightSidebar.contextNotesTodo.notes.empty": "Ainda não há notas. Registre contexto, lembretes ou links.",
"rightSidebar.contextNotesTodo.notes.actions.expand": "Expandir nota",
"rightSidebar.contextNotesTodo.notes.actions.collapse": "Recolher nota",
"rightSidebar.contextNotesTodo.notes.actions.delete": "Excluir nota",
"rightSidebar.contextNotesTodo.notes.actions.pin": "Fixar no contexto do agente",
"rightSidebar.contextNotesTodo.notes.actions.unpin": "Desafixar do contexto do agente",
"rightSidebar.contextNotesTodo.notes.source.selection": "Do chat",
"rightSidebar.contextNotesTodo.notes.source.agent": "Do agente",
"rightSidebar.contextNotesTodo.search.placeholder": "Buscar",
"rightSidebar.contextNotesTodo.search.clear": "Limpar busca",
"rightSidebar.contextNotesTodo.search.noResults": "Nada corresponde a \"{query}\".",
"rightSidebar.contextNotesTodo.toast.deleteNoteFailed": "Falha ao excluir a nota",
"rightSidebar.contextNotesTodo.toast.createNoteFailed": "Falha ao criar a nota",
"rightSidebar.contextNotesTodo.tabs.notes": "Notas",
"rightSidebar.contextNotesTodo.tabs.todos": "Tarefas",
"rightSidebar.contextNotesTodo.tabs.plans": "Planos",
"rightSidebar.contextNotesTodo.plans.actions.back": "Voltar aos planos",
"rightSidebar.contextNotesTodo.tabs.memory": "Memória",
"rightSidebar.contextNotesTodo.sections.label": "Seções do contexto do projeto",
"rightSidebar.contextNotesTodo.sections.resize": "Redimensionar a barra de seções",
"rightSidebar.contextNotesTodo.memory.scope.project": "Projeto",
"rightSidebar.contextNotesTodo.memory.scope.label": "Escopo da memória",
"rightSidebar.contextNotesTodo.memory.scope.global": "Sobre você",
"rightSidebar.contextNotesTodo.memory.type.fact": "fato",
"rightSidebar.contextNotesTodo.memory.badge.new": "novo",
"rightSidebar.contextNotesTodo.memory.flagged": "Retido do agente — parece uma instrução",
"rightSidebar.contextNotesTodo.memory.badge.changed": "alterado",
"rightSidebar.contextNotesTodo.memory.type.preference": "preferência",
"rightSidebar.contextNotesTodo.memory.type.reference": "referência",
"rightSidebar.contextNotesTodo.memory.actions.delete": "Esquecer esta memória",
"rightSidebar.contextNotesTodo.memory.actions.editTitle": "Título da memória",
"rightSidebar.contextNotesTodo.memory.actions.editBody": "Texto da memória",
"rightSidebar.contextNotesTodo.memory.toast.saveFailed": "Não foi possível salvar a memória",
"rightSidebar.contextNotesTodo.memory.toast.deleteFailed": "Não foi possível esquecer a memória",
"rightSidebar.contextNotesTodo.memory.empty.nothing": "O agente ainda não guardou nada aqui.",
"rightSidebar.contextNotesTodo.memory.empty.noMatches": "Nenhuma memória guardada corresponde à sua busca.",
"rightSidebar.contextNotesTodo.memory.empty.noProject": "Abra um projeto para ver o que o agente lembra sobre ele.",
"rightSidebar.contextNotesTodo.memory.empty.unavailable": "Não foi possível carregar a memória guardada. Nada foi perdido — tente novamente.",
"rightSidebar.contextNotesTodo.todo.clearCompleted": "Limpar completadas",
"rightSidebar.contextNotesTodo.todo.inputPlaceholder": "Adicione uma tarefa pendente",
"rightSidebar.contextNotesTodo.todo.addAria": "Adicionar tarefa pendente",
@@ -1532,13 +1580,9 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.todo.actions.delete": "Excluir \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Reordenar \"{text}\"",
"rightSidebar.contextNotesTodo.todo.resizeAria": "Redimensionar lista de tarefas",
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar à sessão atual",
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a uma nova sessão",
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a uma nova sessão de worktree",
"rightSidebar.contextNotesTodo.plans.title": "Planos",
"rightSidebar.contextNotesTodo.plans.filesSingle": "{count} arquivo",
"rightSidebar.contextNotesTodo.plans.filesPlural": "{count} arquivos",
"rightSidebar.contextNotesTodo.plans.importFromFile": "Importar plano de arquivo",
"rightSidebar.contextNotesTodo.plans.empty": "Ainda não há planos salvos.",
"rightSidebar.contextNotesTodo.plans.deletePlan": "Excluir plano",
@@ -1558,6 +1602,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.sentToNewSession": "Tarefa enviada para uma nova sessão",
"rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession": "Tarefa enviada para uma nova sessão de worktree",
"rightSidebar.contextNotesTodo.toast.sendTodoFailed": "Não foi possível enviar a tarefa",
"rightSidebar.contextNotesTodo.toast.updatePlanFailed": "Falha ao atualizar o plano",
"rightSidebar.contextNotesTodo.toast.deletePlanFailed": "Não foi possível excluir o plano",
"rightSidebar.contextNotesTodo.toast.planFileEmpty": "O arquivo do plano está vazio",
"rightSidebar.contextNotesTodo.toast.importPlanFailed": "Não foi possível importar o plano",
@@ -1993,6 +2038,9 @@ export const dict: Record<I18nKey, string> = {
"chat.revert.toast.undo": "Revertido para {preview}",
"chat.revert.toast.redo": "Refeito",
"chat.revert.toast.restored": "Todas as mensagens restauradas",
"chat.toast.opencodeRestartInterrupted.title": "Conversa interrompida",
"chat.toast.opencodeRestartInterrupted.description": "O OpenCode foi reiniciado enquanto uma resposta ainda estava em andamento. Envie uma mensagem para continuar.",
"chat.toast.opencodeRestartInterrupted.openSession": "Abrir sessão",
"chat.errorBoundary.title": "Erro na conversa",
"chat.errorBoundary.description": "A interface da conversa encontrou um erro. Isso pode ter sido causado por um problema temporário de rede ou por dados de mensagem corrompidos.",
"chat.errorBoundary.sessionLabel": "Sessão",
@@ -2983,13 +3031,15 @@ export const dict: Record<I18nKey, string> = {
"contextFileOpen.failure.unreadable": "Failed to open file",
"quota.window.5h": "5-Hour",
"quota.window.7d": "7-Day Limit",
"quota.window.7dSonnet": "7-Day Sonnet Limit",
"quota.window.7dOpus": "7-Day Opus Limit",
"quota.window.weekly": "Weekly Limit",
"quota.window.extraUsage": "Uso adicional",
"quota.window.weekly": "Semanal",
"quota.window.daily": "Daily",
"quota.window.monthly": "Monthly Limit",
"quota.window.monthly": "Mensal",
"quota.window.credits": "Credits",
"quota.window.creditsBalance": "Credits Balance",
"quota.window.monthlyCredits": "Créditos mensais",
"quota.window.purchasedCredits": "Créditos comprados",
"quota.window.freeCredits": "Créditos gratuitos",
"quota.window.billingCycle": "Billing Cycle",
"quota.window.auto": "Auto",
"quota.window.api": "API",
@@ -3032,6 +3082,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': 'fez uma pergunta',
'chat.workStatus.section.contextBreakdown': 'Fontes de contexto',
'chat.workStatus.breakdown.skills': 'Habilidades',
'chat.workStatus.breakdown.pinnedNote': 'nota',
'chat.workStatus.breakdown.unpin': 'Desafixar do contexto',
'chat.workStatus.breakdown.pinnedPlan': 'plano',
'chat.workStatus.breakdown.memory': 'Memória do agente',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} fixado',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} fixados',
'chat.workStatus.breakdown.mcp': 'Servidores MCP',
'chat.workStatus.action.openChanges': 'Abrir alterações',
'chat.workStatus.action.openGit': 'Abrir painel do Git',
@@ -0,0 +1,31 @@
import { describe, expect, test } from 'bun:test';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
const locales = ['en', 'de', 'fr', 'es', 'ja', 'pt-BR', 'uk', 'ko', 'pl', 'zh-CN', 'zh-TW'] as const;
const requiredKeys = [
'settings.page.integrations.title',
'settings.page.integrations.description',
'settings.integrations.messengers.title',
'settings.integrations.messengers.discord.name',
'settings.integrations.messengers.telegram.name',
'settings.integrations.thirdParty.title',
'settings.integrations.thirdParty.actions.install',
'settings.integrations.thirdParty.actions.update',
'settings.integrations.thirdParty.actions.setup',
'settings.integrations.thirdParty.actions.remove',
'settings.integrations.thirdParty.status.notInstalled',
'settings.integrations.thirdParty.opencodeClaude.description',
'settings.integrations.thirdParty.opencodeCommandcode.description',
'settings.integrations.thirdParty.opencodeCursorOauth.description',
] as const;
describe('third-party integration translations', () => {
test('provides every required key in every supported locale', () => {
for (const locale of locales) {
for (const key of requiredKeys) {
expect(thirdPartyIntegrationI18n[locale][key]).toBeTruthy();
}
}
});
});
@@ -0,0 +1,465 @@
/** Third-party integration settings strings — merged into each locale's settings dictionary. */
export const thirdPartyIntegrationI18n = {
en: {
'settings.page.integrations.title': 'Integrations',
'settings.page.integrations.description': 'Add third-party subscriptions to use as OpenChamber providers.',
'settings.integrations.messengers.title': 'Messengers',
'settings.integrations.messengers.info': 'Chat with OpenChamber from Discord or Telegram. These bridges are not available yet.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Connect a Discord bot to chat with OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Connect a Telegram bot to chat with OpenChamber.',
'settings.integrations.thirdParty.title': 'Third-party integrations',
'settings.integrations.thirdParty.info': 'Install a provider plugin, then set up your subscription so OpenChamber can use it.',
'settings.integrations.thirdParty.actions.install': 'Install',
'settings.integrations.thirdParty.actions.update': 'Update',
'settings.integrations.thirdParty.actions.setup': 'Set up',
'settings.integrations.thirdParty.actions.remove': 'Remove',
'settings.integrations.thirdParty.actions.docs': 'Docs',
'settings.integrations.thirdParty.actions.managePlugins': 'Manage plugins',
'settings.integrations.thirdParty.status.notInstalled': 'Not installed',
'settings.integrations.thirdParty.status.installed': 'Installed',
'settings.integrations.thirdParty.status.installedVersion': 'Installed {version}',
'settings.integrations.thirdParty.status.updateAvailable': 'Update available: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Following the latest release',
'settings.integrations.thirdParty.status.projectInstalled': 'Also configured for this project',
'settings.integrations.thirdParty.status.ambiguous': 'Multiple user-wide plugin entries need manual management',
'settings.integrations.thirdParty.status.restartRequired': 'Restart OpenCode before setting up this provider.',
'settings.integrations.thirdParty.status.registryUnavailable': 'Could not check npm right now.',
'settings.integrations.thirdParty.status.providerUnavailable': 'The provider is not available yet. Restart OpenCode and try again.',
'settings.integrations.thirdParty.dialog.remove.title': 'Remove integration',
'settings.integrations.thirdParty.dialog.remove.description': 'Remove {name} from your user-wide OpenCode configuration? The provider will no longer load after OpenCode refreshes.',
'settings.integrations.thirdParty.toast.installed': '{name} installed',
'settings.integrations.thirdParty.toast.updated': '{name} updated',
'settings.integrations.thirdParty.toast.removed': '{name} removed',
'settings.integrations.thirdParty.toast.actionFailed': 'Could not update the integration',
'settings.integrations.thirdParty.toast.providerUnavailable': 'The provider could not be opened yet',
'settings.integrations.thirdParty.toast.restartRequired': 'Restart OpenCode for changes to take effect',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Use your Claude Pro/Max plan — no API keys, no Claude apps.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': '$1 Go Plan: unlimited Laguna S 2.1 + $40 DeepSeek V4 Pro. Sign in, no CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Cursors generous in-house model limits, now in OpenChamber.',
},
de: {
'settings.page.integrations.title': 'Integrationen',
'settings.page.integrations.description': 'Füge Drittanbieter-Abonnements hinzu, um sie als OpenChamber-Provider zu nutzen.',
'settings.integrations.messengers.title': 'Messenger',
'settings.integrations.messengers.info': 'Chatte mit OpenChamber über Discord oder Telegram. Diese Bridges sind noch nicht verfügbar.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Verbinde einen Discord-Bot, um mit OpenChamber zu chatten.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Verbinde einen Telegram-Bot, um mit OpenChamber zu chatten.',
'settings.integrations.thirdParty.title': 'Drittanbieter-Integrationen',
'settings.integrations.thirdParty.info': 'Installiere ein Provider-Plugin und richte dein Abonnement ein, damit OpenChamber es nutzen kann.',
'settings.integrations.thirdParty.actions.install': 'Installieren',
'settings.integrations.thirdParty.actions.update': 'Aktualisieren',
'settings.integrations.thirdParty.actions.setup': 'Einrichten',
'settings.integrations.thirdParty.actions.remove': 'Entfernen',
'settings.integrations.thirdParty.actions.docs': 'Dokumentation',
'settings.integrations.thirdParty.actions.managePlugins': 'Plugins verwalten',
'settings.integrations.thirdParty.status.notInstalled': 'Nicht installiert',
'settings.integrations.thirdParty.status.installed': 'Installiert',
'settings.integrations.thirdParty.status.installedVersion': '{version} installiert',
'settings.integrations.thirdParty.status.updateAvailable': 'Aktualisierung verfügbar: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Folgt der neuesten Version',
'settings.integrations.thirdParty.status.projectInstalled': 'Auch für dieses Projekt konfiguriert',
'settings.integrations.thirdParty.status.ambiguous': 'Mehrere benutzerweite Plugin-Einträge müssen manuell verwaltet werden',
'settings.integrations.thirdParty.status.restartRequired': 'Starte OpenCode neu, bevor du diesen Provider einrichtest.',
'settings.integrations.thirdParty.status.registryUnavailable': 'npm konnte gerade nicht geprüft werden.',
'settings.integrations.thirdParty.status.providerUnavailable': 'Der Provider ist noch nicht verfügbar. Starte OpenCode neu und versuche es erneut.',
'settings.integrations.thirdParty.dialog.remove.title': 'Integration entfernen',
'settings.integrations.thirdParty.dialog.remove.description': '{name} aus deiner benutzerweiten OpenCode-Konfiguration entfernen? Der Provider wird nach der Aktualisierung von OpenCode nicht mehr geladen.',
'settings.integrations.thirdParty.toast.installed': '{name} installiert',
'settings.integrations.thirdParty.toast.updated': '{name} aktualisiert',
'settings.integrations.thirdParty.toast.removed': '{name} entfernt',
'settings.integrations.thirdParty.toast.actionFailed': 'Die Integration konnte nicht aktualisiert werden',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Der Provider konnte noch nicht geöffnet werden',
'settings.integrations.thirdParty.toast.restartRequired': 'Starte OpenCode neu, damit die Änderungen wirksam werden',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Nutze deinen Claude-Pro/Max-Plan — ohne API-Keys, ohne Claude-Apps.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go-Plan für 1 $: unbegrenztes Laguna S 2.1 + 40 $ DeepSeek V4 Pro. Anmelden, kein CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Die großzügigen Limits der Cursor-eigenen Modelle jetzt in OpenChamber.',
},
fr: {
'settings.page.integrations.title': 'Intégrations',
'settings.page.integrations.description': 'Ajoutez des abonnements tiers à utiliser comme fournisseurs OpenChamber.',
'settings.integrations.messengers.title': 'Messagers',
'settings.integrations.messengers.info': 'Discutez avec OpenChamber depuis Discord ou Telegram. Ces ponts ne sont pas encore disponibles.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Connectez un bot Discord pour discuter avec OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Connectez un bot Telegram pour discuter avec OpenChamber.',
'settings.integrations.thirdParty.title': 'Intégrations tierces',
'settings.integrations.thirdParty.info': 'Installez un plugin de fournisseur, puis configurez votre abonnement pour quOpenChamber puisse lutiliser.',
'settings.integrations.thirdParty.actions.install': 'Installer',
'settings.integrations.thirdParty.actions.update': 'Mettre à jour',
'settings.integrations.thirdParty.actions.setup': 'Configurer',
'settings.integrations.thirdParty.actions.remove': 'Supprimer',
'settings.integrations.thirdParty.actions.docs': 'Documentation',
'settings.integrations.thirdParty.actions.managePlugins': 'Gérer les plugins',
'settings.integrations.thirdParty.status.notInstalled': 'Non installé',
'settings.integrations.thirdParty.status.installed': 'Installé',
'settings.integrations.thirdParty.status.installedVersion': '{version} installé',
'settings.integrations.thirdParty.status.updateAvailable': 'Mise à jour disponible : {version}',
'settings.integrations.thirdParty.status.unpinned': 'Suit la dernière version',
'settings.integrations.thirdParty.status.projectInstalled': 'Également configuré pour ce projet',
'settings.integrations.thirdParty.status.ambiguous': 'Plusieurs entrées de plugin globales doivent être gérées manuellement',
'settings.integrations.thirdParty.status.restartRequired': 'Redémarrez OpenCode avant de configurer ce fournisseur.',
'settings.integrations.thirdParty.status.registryUnavailable': 'Impossible de vérifier npm pour le moment.',
'settings.integrations.thirdParty.status.providerUnavailable': 'Le fournisseur nest pas encore disponible. Redémarrez OpenCode et réessayez.',
'settings.integrations.thirdParty.dialog.remove.title': 'Supprimer lintégration',
'settings.integrations.thirdParty.dialog.remove.description': 'Supprimer {name} de votre configuration OpenCode globale ? Le fournisseur ne sera plus chargé après lactualisation dOpenCode.',
'settings.integrations.thirdParty.toast.installed': '{name} installé',
'settings.integrations.thirdParty.toast.updated': '{name} mis à jour',
'settings.integrations.thirdParty.toast.removed': '{name} supprimé',
'settings.integrations.thirdParty.toast.actionFailed': 'Impossible de mettre à jour lintégration',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Le fournisseur na pas encore pu être ouvert',
'settings.integrations.thirdParty.toast.restartRequired': 'Redémarrez OpenCode pour que les modifications prennent effet',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Utilisez votre forfait Claude Pro/Max — sans clés API, sans apps Claude.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go Plan à 1 $ : Laguna S 2.1 illimité + 40 $ DeepSeek V4 Pro. Connectez-vous, sans CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Les généreuses limites des modèles internes Cursor, désormais dans OpenChamber.',
},
es: {
'settings.page.integrations.title': 'Integraciones',
'settings.page.integrations.description': 'Añade suscripciones de terceros para usarlas como proveedores de OpenChamber.',
'settings.integrations.messengers.title': 'Mensajeros',
'settings.integrations.messengers.info': 'Chatea con OpenChamber desde Discord o Telegram. Estos puentes aún no están disponibles.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Conecta un bot de Discord para chatear con OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Conecta un bot de Telegram para chatear con OpenChamber.',
'settings.integrations.thirdParty.title': 'Integraciones de terceros',
'settings.integrations.thirdParty.info': 'Instala un plugin de proveedor y configura tu suscripción para que OpenChamber pueda usarla.',
'settings.integrations.thirdParty.actions.install': 'Instalar',
'settings.integrations.thirdParty.actions.update': 'Actualizar',
'settings.integrations.thirdParty.actions.setup': 'Configurar',
'settings.integrations.thirdParty.actions.remove': 'Quitar',
'settings.integrations.thirdParty.actions.docs': 'Documentación',
'settings.integrations.thirdParty.actions.managePlugins': 'Administrar plugins',
'settings.integrations.thirdParty.status.notInstalled': 'No instalado',
'settings.integrations.thirdParty.status.installed': 'Instalado',
'settings.integrations.thirdParty.status.installedVersion': '{version} instalado',
'settings.integrations.thirdParty.status.updateAvailable': 'Actualización disponible: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Sigue la versión más reciente',
'settings.integrations.thirdParty.status.projectInstalled': 'También configurado para este proyecto',
'settings.integrations.thirdParty.status.ambiguous': 'Hay varias entradas de plugin globales que requieren gestión manual',
'settings.integrations.thirdParty.status.restartRequired': 'Reinicia OpenCode antes de configurar este proveedor.',
'settings.integrations.thirdParty.status.registryUnavailable': 'No se pudo comprobar npm ahora mismo.',
'settings.integrations.thirdParty.status.providerUnavailable': 'El proveedor todavía no está disponible. Reinicia OpenCode e inténtalo de nuevo.',
'settings.integrations.thirdParty.dialog.remove.title': 'Quitar integración',
'settings.integrations.thirdParty.dialog.remove.description': '¿Quitar {name} de tu configuración global de OpenCode? El proveedor dejará de cargarse después de actualizar OpenCode.',
'settings.integrations.thirdParty.toast.installed': '{name} instalado',
'settings.integrations.thirdParty.toast.updated': '{name} actualizado',
'settings.integrations.thirdParty.toast.removed': '{name} eliminado',
'settings.integrations.thirdParty.toast.actionFailed': 'No se pudo actualizar la integración',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Todavía no se pudo abrir el proveedor',
'settings.integrations.thirdParty.toast.restartRequired': 'Reinicia OpenCode para que los cambios surtan efecto',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Usa tu plan Claude Pro/Max: sin claves API ni apps de Claude.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go Plan por 1 $: Laguna S 2.1 ilimitado + 40 $ de DeepSeek V4 Pro. Entra, sin CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Los generosos límites de los modelos internos de Cursor, ahora en OpenChamber.',
},
ja: {
'settings.page.integrations.title': '連携',
'settings.page.integrations.description': 'サードパーティのサブスクリプションを追加して、OpenChamber のプロバイダーとして使います。',
'settings.integrations.messengers.title': 'メッセンジャー',
'settings.integrations.messengers.info': 'Discord または Telegram から OpenChamber とチャットできます。これらの連携はまだ利用できません。',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Discord ボットを接続して OpenChamber とチャットします。',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Telegram ボットを接続して OpenChamber とチャットします。',
'settings.integrations.thirdParty.title': 'サードパーティー連携',
'settings.integrations.thirdParty.info': 'プロバイダープラグインをインストールし、サブスクリプションを設定して OpenChamber で使えるようにします。',
'settings.integrations.thirdParty.actions.install': 'インストール',
'settings.integrations.thirdParty.actions.update': '更新',
'settings.integrations.thirdParty.actions.setup': '設定',
'settings.integrations.thirdParty.actions.remove': '削除',
'settings.integrations.thirdParty.actions.docs': 'ドキュメント',
'settings.integrations.thirdParty.actions.managePlugins': 'プラグインを管理',
'settings.integrations.thirdParty.status.notInstalled': '未インストール',
'settings.integrations.thirdParty.status.installed': 'インストール済み',
'settings.integrations.thirdParty.status.installedVersion': '{version} をインストール済み',
'settings.integrations.thirdParty.status.updateAvailable': '更新があります: {version}',
'settings.integrations.thirdParty.status.unpinned': '最新リリースを追跡中',
'settings.integrations.thirdParty.status.projectInstalled': 'このプロジェクトにも設定済み',
'settings.integrations.thirdParty.status.ambiguous': '複数のユーザー全体プラグインエントリーは手動で管理する必要があります',
'settings.integrations.thirdParty.status.restartRequired': 'このプロバイダーを設定する前に OpenCode を再起動してください。',
'settings.integrations.thirdParty.status.registryUnavailable': '現在 npm を確認できません。',
'settings.integrations.thirdParty.status.providerUnavailable': 'プロバイダーはまだ利用できません。OpenCode を再起動して再試行してください。',
'settings.integrations.thirdParty.dialog.remove.title': '連携を削除',
'settings.integrations.thirdParty.dialog.remove.description': '{name} をユーザー全体の OpenCode 設定から削除しますか?OpenCode の更新後、このプロバイダーは読み込まれなくなります。',
'settings.integrations.thirdParty.toast.installed': '{name} をインストールしました',
'settings.integrations.thirdParty.toast.updated': '{name} を更新しました',
'settings.integrations.thirdParty.toast.removed': '{name} を削除しました',
'settings.integrations.thirdParty.toast.actionFailed': '連携を更新できませんでした',
'settings.integrations.thirdParty.toast.providerUnavailable': 'プロバイダーをまだ開けませんでした',
'settings.integrations.thirdParty.toast.restartRequired': '変更を反映するには OpenCode を再起動してください',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Claude Pro/Max プランを利用 — API キーも Claude アプリも不要。',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': '1ドルの Go PlanLaguna S 2.1 無制限 + DeepSeek V4 Pro 40ドル分。ログインするだけで CLI 不要。',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Cursor 内蔵モデルの余裕ある制限が、OpenChamber で使えます。',
},
ko: {
'settings.page.integrations.title': '통합',
'settings.page.integrations.description': '타사 구독을 추가해 OpenChamber 프로바이더로 사용하세요.',
'settings.integrations.messengers.title': '메신저',
'settings.integrations.messengers.info': 'Discord 또는 Telegram에서 OpenChamber와 채팅하세요. 이 브리지는 아직 사용할 수 없습니다.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Discord 봇을 연결해 OpenChamber와 채팅하세요.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Telegram 봇을 연결해 OpenChamber와 채팅하세요.',
'settings.integrations.thirdParty.title': '서드파티 통합',
'settings.integrations.thirdParty.info': '프로바이더 플러그인을 설치한 뒤 구독을 설정하면 OpenChamber에서 사용할 수 있습니다.',
'settings.integrations.thirdParty.actions.install': '설치',
'settings.integrations.thirdParty.actions.update': '업데이트',
'settings.integrations.thirdParty.actions.setup': '설정',
'settings.integrations.thirdParty.actions.remove': '제거',
'settings.integrations.thirdParty.actions.docs': '문서',
'settings.integrations.thirdParty.actions.managePlugins': '플러그인 관리',
'settings.integrations.thirdParty.status.notInstalled': '설치되지 않음',
'settings.integrations.thirdParty.status.installed': '설치됨',
'settings.integrations.thirdParty.status.installedVersion': '{version} 설치됨',
'settings.integrations.thirdParty.status.updateAvailable': '업데이트 가능: {version}',
'settings.integrations.thirdParty.status.unpinned': '최신 릴리스 추적 중',
'settings.integrations.thirdParty.status.projectInstalled': '이 프로젝트에도 구성됨',
'settings.integrations.thirdParty.status.ambiguous': '여러 사용자 전체 플러그인 항목은 수동으로 관리해야 합니다',
'settings.integrations.thirdParty.status.restartRequired': '이 프로바이더를 설정하기 전에 OpenCode를 다시 시작하세요.',
'settings.integrations.thirdParty.status.registryUnavailable': '지금은 npm을 확인할 수 없습니다.',
'settings.integrations.thirdParty.status.providerUnavailable': '프로바이더를 아직 사용할 수 없습니다. OpenCode를 다시 시작한 후 재시도하세요.',
'settings.integrations.thirdParty.dialog.remove.title': '통합 제거',
'settings.integrations.thirdParty.dialog.remove.description': '사용자 전체 OpenCode 구성에서 {name}을(를) 제거할까요? OpenCode가 새로 고쳐진 후 프로바이더가 더 이상 로드되지 않습니다.',
'settings.integrations.thirdParty.toast.installed': '{name} 설치됨',
'settings.integrations.thirdParty.toast.updated': '{name} 업데이트됨',
'settings.integrations.thirdParty.toast.removed': '{name} 제거됨',
'settings.integrations.thirdParty.toast.actionFailed': '통합을 업데이트하지 못했습니다',
'settings.integrations.thirdParty.toast.providerUnavailable': '프로바이더를 아직 열 수 없습니다',
'settings.integrations.thirdParty.toast.restartRequired': '변경 사항을 적용하려면 OpenCode를 다시 시작하세요',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Claude Pro/Max 요금제를 사용하세요. API 키와 Claude 앱은 필요 없습니다.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': '1달러 Go Plan: Laguna S 2.1 무제한 + DeepSeek V4 Pro 40달러. 로그인만 하면 되고 CLI는 필요 없습니다.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Cursor 자체 모델의 넉넉한 한도를 이제 OpenChamber에서.',
},
pl: {
'settings.page.integrations.title': 'Integracje',
'settings.page.integrations.description': 'Dodaj subskrypcje zewnętrzne, aby używać ich jako dostawców OpenChamber.',
'settings.integrations.messengers.title': 'Komunikatory',
'settings.integrations.messengers.info': 'Czatuj z OpenChamber przez Discord lub Telegram. Te mosty nie są jeszcze dostępne.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Połącz bota Discord, aby czatować z OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Połącz bota Telegram, aby czatować z OpenChamber.',
'settings.integrations.thirdParty.title': 'Integracje zewnętrzne',
'settings.integrations.thirdParty.info': 'Zainstaluj wtyczkę dostawcy, a następnie skonfiguruj subskrypcję, aby OpenChamber mógł z niej korzystać.',
'settings.integrations.thirdParty.actions.install': 'Zainstaluj',
'settings.integrations.thirdParty.actions.update': 'Aktualizuj',
'settings.integrations.thirdParty.actions.setup': 'Skonfiguruj',
'settings.integrations.thirdParty.actions.remove': 'Usuń',
'settings.integrations.thirdParty.actions.docs': 'Dokumentacja',
'settings.integrations.thirdParty.actions.managePlugins': 'Zarządzaj wtyczkami',
'settings.integrations.thirdParty.status.notInstalled': 'Nie zainstalowano',
'settings.integrations.thirdParty.status.installed': 'Zainstalowano',
'settings.integrations.thirdParty.status.installedVersion': 'Zainstalowano {version}',
'settings.integrations.thirdParty.status.updateAvailable': 'Dostępna aktualizacja: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Śledzi najnowsze wydanie',
'settings.integrations.thirdParty.status.projectInstalled': 'Skonfigurowano także dla tego projektu',
'settings.integrations.thirdParty.status.ambiguous': 'Wiele globalnych wpisów wtyczki wymaga ręcznego zarządzania',
'settings.integrations.thirdParty.status.restartRequired': 'Uruchom ponownie OpenCode przed konfiguracją tego dostawcy.',
'settings.integrations.thirdParty.status.registryUnavailable': 'Nie można teraz sprawdzić npm.',
'settings.integrations.thirdParty.status.providerUnavailable': 'Dostawca nie jest jeszcze dostępny. Uruchom ponownie OpenCode i spróbuj ponownie.',
'settings.integrations.thirdParty.dialog.remove.title': 'Usuń integrację',
'settings.integrations.thirdParty.dialog.remove.description': 'Usunąć {name} z globalnej konfiguracji OpenCode? Dostawca przestanie być ładowany po odświeżeniu OpenCode.',
'settings.integrations.thirdParty.toast.installed': 'Zainstalowano {name}',
'settings.integrations.thirdParty.toast.updated': 'Zaktualizowano {name}',
'settings.integrations.thirdParty.toast.removed': 'Usunięto {name}',
'settings.integrations.thirdParty.toast.actionFailed': 'Nie udało się zaktualizować integracji',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Nie można jeszcze otworzyć dostawcy',
'settings.integrations.thirdParty.toast.restartRequired': 'Uruchom ponownie OpenCode, aby zastosować zmiany',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Korzystaj z planu Claude Pro/Max — bez kluczy API i aplikacji Claude.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go Plan za 1 $: nielimitowane Laguna S 2.1 + 40 $ DeepSeek V4 Pro. Zaloguj się, bez CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Hojne limity wewnętrznych modeli Cursor teraz w OpenChamber.',
},
'pt-BR': {
'settings.page.integrations.title': 'Integrações',
'settings.page.integrations.description': 'Adicione assinaturas de terceiros para usar como provedores do OpenChamber.',
'settings.integrations.messengers.title': 'Mensageiros',
'settings.integrations.messengers.info': 'Converse com o OpenChamber pelo Discord ou Telegram. Essas pontes ainda não estão disponíveis.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Conecte um bot do Discord para conversar com o OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Conecte um bot do Telegram para conversar com o OpenChamber.',
'settings.integrations.thirdParty.title': 'Integrações de terceiros',
'settings.integrations.thirdParty.info': 'Instale um plugin de provedor e configure sua assinatura para o OpenChamber poder usá-la.',
'settings.integrations.thirdParty.actions.install': 'Instalar',
'settings.integrations.thirdParty.actions.update': 'Atualizar',
'settings.integrations.thirdParty.actions.setup': 'Configurar',
'settings.integrations.thirdParty.actions.remove': 'Remover',
'settings.integrations.thirdParty.actions.docs': 'Documentação',
'settings.integrations.thirdParty.actions.managePlugins': 'Gerenciar plugins',
'settings.integrations.thirdParty.status.notInstalled': 'Não instalado',
'settings.integrations.thirdParty.status.installed': 'Instalado',
'settings.integrations.thirdParty.status.installedVersion': '{version} instalado',
'settings.integrations.thirdParty.status.updateAvailable': 'Atualização disponível: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Acompanha a versão mais recente',
'settings.integrations.thirdParty.status.projectInstalled': 'Também configurado para este projeto',
'settings.integrations.thirdParty.status.ambiguous': 'Várias entradas de plugin globais precisam de gerenciamento manual',
'settings.integrations.thirdParty.status.restartRequired': 'Reinicie o OpenCode antes de configurar este provedor.',
'settings.integrations.thirdParty.status.registryUnavailable': 'Não foi possível verificar o npm agora.',
'settings.integrations.thirdParty.status.providerUnavailable': 'O provedor ainda não está disponível. Reinicie o OpenCode e tente novamente.',
'settings.integrations.thirdParty.dialog.remove.title': 'Remover integração',
'settings.integrations.thirdParty.dialog.remove.description': 'Remover {name} da sua configuração global do OpenCode? O provedor deixará de ser carregado após a atualização do OpenCode.',
'settings.integrations.thirdParty.toast.installed': '{name} instalado',
'settings.integrations.thirdParty.toast.updated': '{name} atualizado',
'settings.integrations.thirdParty.toast.removed': '{name} removido',
'settings.integrations.thirdParty.toast.actionFailed': 'Não foi possível atualizar a integração',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Ainda não foi possível abrir o provedor',
'settings.integrations.thirdParty.toast.restartRequired': 'Reinicie o OpenCode para que as alterações entrem em vigor',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Use seu plano Claude Pro/Max — sem chaves de API nem apps da Claude.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go Plan por US$ 1: Laguna S 2.1 ilimitado + US$ 40 de DeepSeek V4 Pro. Entre, sem CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Os limites generosos dos modelos internos do Cursor, agora no OpenChamber.',
},
uk: {
'settings.page.integrations.title': 'Інтеграції',
'settings.page.integrations.description': 'Додайте сторонні підписки, щоб використовувати їх як провайдери OpenChamber.',
'settings.integrations.messengers.title': 'Месенджери',
'settings.integrations.messengers.info': 'Спілкуйтеся з OpenChamber у Discord або Telegram. Ці мости ще недоступні.',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': 'Підключіть бота Discord, щоб спілкуватися з OpenChamber.',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': 'Підключіть бота Telegram, щоб спілкуватися з OpenChamber.',
'settings.integrations.thirdParty.title': 'Сторонні інтеграції',
'settings.integrations.thirdParty.info': 'Установіть плагін провайдера, а потім налаштуйте підписку, щоб OpenChamber міг її використовувати.',
'settings.integrations.thirdParty.actions.install': 'Встановити',
'settings.integrations.thirdParty.actions.update': 'Оновити',
'settings.integrations.thirdParty.actions.setup': 'Налаштувати',
'settings.integrations.thirdParty.actions.remove': 'Видалити',
'settings.integrations.thirdParty.actions.docs': 'Документація',
'settings.integrations.thirdParty.actions.managePlugins': 'Керувати плагінами',
'settings.integrations.thirdParty.status.notInstalled': 'Не встановлено',
'settings.integrations.thirdParty.status.installed': 'Встановлено',
'settings.integrations.thirdParty.status.installedVersion': 'Встановлено {version}',
'settings.integrations.thirdParty.status.updateAvailable': 'Доступне оновлення: {version}',
'settings.integrations.thirdParty.status.unpinned': 'Відстежує найновіший випуск',
'settings.integrations.thirdParty.status.projectInstalled': 'Також налаштовано для цього проєкту',
'settings.integrations.thirdParty.status.ambiguous': 'Кілька глобальних записів плагіна потребують ручного керування',
'settings.integrations.thirdParty.status.restartRequired': 'Перезапустіть OpenCode перед налаштуванням цього провайдера.',
'settings.integrations.thirdParty.status.registryUnavailable': 'Зараз не вдалося перевірити npm.',
'settings.integrations.thirdParty.status.providerUnavailable': 'Провайдер ще недоступний. Перезапустіть OpenCode й спробуйте знову.',
'settings.integrations.thirdParty.dialog.remove.title': 'Видалити інтеграцію',
'settings.integrations.thirdParty.dialog.remove.description': 'Видалити {name} з вашої глобальної конфігурації OpenCode? Після оновлення OpenCode провайдер більше не завантажуватиметься.',
'settings.integrations.thirdParty.toast.installed': '{name} встановлено',
'settings.integrations.thirdParty.toast.updated': '{name} оновлено',
'settings.integrations.thirdParty.toast.removed': '{name} видалено',
'settings.integrations.thirdParty.toast.actionFailed': 'Не вдалося оновити інтеграцію',
'settings.integrations.thirdParty.toast.providerUnavailable': 'Провайдера ще не вдалося відкрити',
'settings.integrations.thirdParty.toast.restartRequired': 'Перезапустіть OpenCode, щоб застосувати зміни',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': 'Claude Pro/Max за підпискою — без API-ключів і без додатків Claude.',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': 'Go Plan за $1: безліміт Laguna S 2.1 і $40 на DeepSeek V4 Pro. Вхід без CLI.',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Щедрі ліміти внутрішніх моделей Cursor — тепер в OpenChamber.',
},
'zh-CN': {
'settings.page.integrations.title': '集成',
'settings.page.integrations.description': '添加第三方订阅,将其用作 OpenChamber 提供商。',
'settings.integrations.messengers.title': '即时通讯',
'settings.integrations.messengers.info': '通过 Discord 或 Telegram 与 OpenChamber 聊天。这些桥接尚不可用。',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': '连接 Discord 机器人以与 OpenChamber 聊天。',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': '连接 Telegram 机器人以与 OpenChamber 聊天。',
'settings.integrations.thirdParty.title': '第三方集成',
'settings.integrations.thirdParty.info': '安装提供商插件并设置订阅,以便 OpenChamber 可以使用它。',
'settings.integrations.thirdParty.actions.install': '安装',
'settings.integrations.thirdParty.actions.update': '更新',
'settings.integrations.thirdParty.actions.setup': '设置',
'settings.integrations.thirdParty.actions.remove': '移除',
'settings.integrations.thirdParty.actions.docs': '文档',
'settings.integrations.thirdParty.actions.managePlugins': '管理插件',
'settings.integrations.thirdParty.status.notInstalled': '未安装',
'settings.integrations.thirdParty.status.installed': '已安装',
'settings.integrations.thirdParty.status.installedVersion': '已安装 {version}',
'settings.integrations.thirdParty.status.updateAvailable': '有可用更新:{version}',
'settings.integrations.thirdParty.status.unpinned': '跟踪最新版本',
'settings.integrations.thirdParty.status.projectInstalled': '也已为此项目配置',
'settings.integrations.thirdParty.status.ambiguous': '多个用户级插件条目需要手动管理',
'settings.integrations.thirdParty.status.restartRequired': '请先重启 OpenCode,再设置此提供商。',
'settings.integrations.thirdParty.status.registryUnavailable': '当前无法检查 npm。',
'settings.integrations.thirdParty.status.providerUnavailable': '该提供商尚不可用。请重启 OpenCode 后重试。',
'settings.integrations.thirdParty.dialog.remove.title': '移除集成',
'settings.integrations.thirdParty.dialog.remove.description': '要从用户级 OpenCode 配置中移除 {name} 吗?OpenCode 刷新后将不再加载该提供商。',
'settings.integrations.thirdParty.toast.installed': '已安装 {name}',
'settings.integrations.thirdParty.toast.updated': '已更新 {name}',
'settings.integrations.thirdParty.toast.removed': '已移除 {name}',
'settings.integrations.thirdParty.toast.actionFailed': '无法更新集成',
'settings.integrations.thirdParty.toast.providerUnavailable': '暂时无法打开该提供商',
'settings.integrations.thirdParty.toast.restartRequired': '请重启 OpenCode 以使更改生效',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': '使用 Claude Pro/Max 套餐——无需 API 密钥,也无需 Claude 应用。',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': '1 美元 Go Plan:无限 Laguna S 2.1,另含 40 美元 DeepSeek V4 Pro。登录即可,无需 CLI。',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Cursor 内部模型的充足额度,现已可用于 OpenChamber。',
},
'zh-TW': {
'settings.page.integrations.title': '整合',
'settings.page.integrations.description': '新增第三方訂閱,將其用作 OpenChamber 供應商。',
'settings.integrations.messengers.title': '即時通訊',
'settings.integrations.messengers.info': '透過 Discord 或 Telegram 與 OpenChamber 聊天。這些橋接尚不可用。',
'settings.integrations.messengers.discord.name': 'Discord',
'settings.integrations.messengers.discord.description': '連接 Discord 機器人以與 OpenChamber 聊天。',
'settings.integrations.messengers.telegram.name': 'Telegram',
'settings.integrations.messengers.telegram.description': '連接 Telegram 機器人以與 OpenChamber 聊天。',
'settings.integrations.thirdParty.title': '第三方整合',
'settings.integrations.thirdParty.info': '安裝供應商外掛並設定訂閱,以便 OpenChamber 可以使用它。',
'settings.integrations.thirdParty.actions.install': '安裝',
'settings.integrations.thirdParty.actions.update': '更新',
'settings.integrations.thirdParty.actions.setup': '設定',
'settings.integrations.thirdParty.actions.remove': '移除',
'settings.integrations.thirdParty.actions.docs': '文件',
'settings.integrations.thirdParty.actions.managePlugins': '管理外掛',
'settings.integrations.thirdParty.status.notInstalled': '未安裝',
'settings.integrations.thirdParty.status.installed': '已安裝',
'settings.integrations.thirdParty.status.installedVersion': '已安裝 {version}',
'settings.integrations.thirdParty.status.updateAvailable': '有可用更新:{version}',
'settings.integrations.thirdParty.status.unpinned': '追蹤最新版本',
'settings.integrations.thirdParty.status.projectInstalled': '也已為此專案設定',
'settings.integrations.thirdParty.status.ambiguous': '多個使用者層級外掛項目需要手動管理',
'settings.integrations.thirdParty.status.restartRequired': '請先重新啟動 OpenCode,再設定此供應商。',
'settings.integrations.thirdParty.status.registryUnavailable': '目前無法檢查 npm。',
'settings.integrations.thirdParty.status.providerUnavailable': '供應商尚不可用。請重新啟動 OpenCode 後再試一次。',
'settings.integrations.thirdParty.dialog.remove.title': '移除整合',
'settings.integrations.thirdParty.dialog.remove.description': '要從使用者層級 OpenCode 設定中移除 {name} 嗎?OpenCode 重新整理後將不再載入此供應商。',
'settings.integrations.thirdParty.toast.installed': '已安裝 {name}',
'settings.integrations.thirdParty.toast.updated': '已更新 {name}',
'settings.integrations.thirdParty.toast.removed': '已移除 {name}',
'settings.integrations.thirdParty.toast.actionFailed': '無法更新整合',
'settings.integrations.thirdParty.toast.providerUnavailable': '暫時無法開啟供應商',
'settings.integrations.thirdParty.toast.restartRequired': '請重新啟動 OpenCode 以使變更生效',
'settings.integrations.thirdParty.opencodeClaude.name': 'Claude Code',
'settings.integrations.thirdParty.opencodeClaude.description': '使用 Claude Pro/Max 方案——無需 API 金鑰,也無需 Claude 應用程式。',
'settings.integrations.thirdParty.opencodeCommandcode.name': 'Command Code',
'settings.integrations.thirdParty.opencodeCommandcode.description': '1 美元 Go Plan:無限 Laguna S 2.1,另含 40 美元 DeepSeek V4 Pro。登入即可,無需 CLI。',
'settings.integrations.thirdParty.opencodeCursorOauth.name': 'Cursor',
'settings.integrations.thirdParty.opencodeCursorOauth.description': 'Cursor 內部模型的充足額度,現已可用於 OpenChamber。',
},
} as const;
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Відстеження використання OpenCode Go',
'settings.providers.page.openCodeGo.description': 'Підключіть панель OpenCode Go, щоб бачити ковзну, тижневу та місячну квоту.',
@@ -864,16 +865,26 @@ export const settingsDict = {
"settings.skills.catalog.page.mode.manual": "Вручну",
"settings.skills.catalog.page.mode.external": "зовнішній",
"settings.skills.catalog.page.title": "Каталог навичок",
'settings.skills.catalog.page.subtitle': 'Встановлюйте готові скіли з курованих репозиторіїв або додайте власне джерело.',
'settings.skills.catalog.page.section.sources': 'Джерела',
'settings.skills.catalog.page.searchAllPlaceholder': 'Пошук скілів у всіх джерелах…',
'settings.skills.catalog.page.search.clear': 'Очистити пошук',
'settings.skills.catalog.page.source.skillsCount': 'Скілів: {count}',
'settings.skills.catalog.page.source.stars': 'Зірок: {count}',
'settings.skills.catalog.page.source.updated': 'Оновлено {time}',
'settings.skills.catalog.page.source.addOwnTitle': 'Додати власне джерело',
'settings.skills.catalog.page.source.addOwnDescription': 'Будь-який git-репозиторій зі скілами',
'settings.skills.catalog.page.source.viewRepo': 'Відкрити репозиторій на GitHub',
'settings.skills.catalog.page.skill.viewOnGithub': 'Переглянути скіл на GitHub',
'settings.skills.catalog.page.list.searchTitle': 'Результати пошуку',
"settings.skills.catalog.page.section.sourceRepository": "Репозиторій вихідного коду",
"settings.skills.catalog.page.field.selectSourcePlaceholder": "Виберіть джерело",
"settings.skills.catalog.page.actions.refreshTitle": "Оновити",
"settings.skills.catalog.page.actions.removeCatalogTitle": "Видалити каталог",
"settings.skills.catalog.page.actions.addCatalog": "Додати каталог",
"settings.skills.catalog.page.actions.removeCatalog": "Видалити каталог",
"settings.skills.catalog.page.actions.loadMoreSkills": "Завантажити додаткові навички",
"settings.skills.catalog.page.loading.catalog": "Завантаження...",
"settings.skills.catalog.page.loading.skills": "Завантаження навичок...",
"settings.skills.catalog.page.loading.more": "Завантаження...",
"settings.skills.catalog.page.foundCount": "Знайдено навички {count}",
"settings.skills.catalog.page.error.catalogTitle": "Помилка каталогу",
"settings.skills.catalog.page.empty.noSkillsTitle": "Навички не знайдено",
@@ -881,7 +892,6 @@ export const settingsDict = {
"settings.skills.catalog.page.badge.installed": "встановлено ({scope})",
"settings.skills.catalog.page.badge.notInstallable": "не встановлюється",
"settings.skills.catalog.page.badge.unknown": "невідомий",
"settings.skills.catalog.page.byOwnerPrefix": "за",
"settings.skills.catalog.page.removeDialog.title": "Видалити каталог",
"settings.skills.catalog.page.removeDialog.description": "Ви впевнені, що хочете видалити цей каталог?",
"settings.openchamber.passkeys.title": "Ключі доступу",
@@ -980,6 +990,9 @@ export const settingsDict = {
"settings.openchamber.tools.field.agentWebTool": "Інструмент OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolAria": "Увімкнути інструмент OpenChamber Web",
"settings.openchamber.tools.field.agentWebToolInfo": "Дозвольте агентам переглядати сторінку в панелі браузера OpenChamber і взаємодіяти з нею: відкривати URL, читати вміст, клікати, вводити текст, гортати та перемикатися між мобільним і десктопним виглядом. Додає невеликий опис інструмента до кожної сесії. Застосовується після перезапуску OpenCode.",
"settings.openchamber.tools.field.agentMemoryTool": "Інструмент памʼяті агента",
"settings.openchamber.tools.field.agentMemoryToolAria": "Інструмент памʼяті агента",
"settings.openchamber.tools.field.agentMemoryToolInfo": "Дозволяє агентам зберігати вивчене між сесіями у двох сховищах: що правдиве про вас і що правдиве про кожен проєкт. Сесії отримують перелік заголовків, щоб агент міг прочитати потрібний запис. Вимкнення прибирає інструмент, вкладку «Памʼять» і індекс у сесії. Діє після перезапуску OpenCode.",
"settings.openchamber.opencodeCli.tooltipPrefix": "Додатковий абсолютний шлях до",
"settings.openchamber.opencodeCli.tooltipSuffix": "бінарного файлу.",
"settings.openchamber.opencodeCli.field.binaryPath": "Шлях до бінарного файлу OpenCode",
@@ -1160,6 +1173,7 @@ export const settingsDict = {
"settings.usage.page.header.providerUsage": "Використання {provider}",
"settings.usage.page.header.refreshing": "Оновлення використання...",
"settings.usage.page.header.lastUpdated": "Останнє оновлення: {time}",
"settings.usage.page.header.lastUpdatedWithPlan": "План: {plan} · Останнє оновлення: {time}",
"settings.usage.page.options.showInWorkStatusAria": "Показувати в панелі статусу роботи",
"settings.usage.page.options.showInWorkStatus": "Показувати в панелі статусу роботи",
"settings.usage.page.options.showInWorkStatusTooltip": "Якщо ввімкнути, використання цього провайдера буде видно в панелі статусу роботи.",
@@ -1326,13 +1340,18 @@ export const settingsDict = {
"settings.providers.page.custom.title": "Власний провайдер",
"settings.providers.page.custom.editTitle": "Редагувати власного провайдера",
"settings.providers.page.custom.description": "Додайте OpenAI-сумісного провайдера з базовою URL-адресою, обліковими даними та списком моделей. Зберігається в конфігурації OpenCode й працює в чаті як будь-який інший провайдер.",
"settings.providers.page.custom.description": "Додайте провайдера з базовою URL-адресою, обліковими даними, списком моделей і підтримуваним протоколом API. Зберігається в конфігурації OpenCode для використання в чаті.",
"settings.providers.page.custom.field.providerID.label": "ID провайдера",
"settings.providers.page.custom.field.providerID.placeholder": "mij-provider",
"settings.providers.page.custom.field.providerID.info": "Малі літери, цифри, дефіси та підкреслення. Використовується як ID провайдера OpenCode.",
"settings.providers.page.custom.field.name.label": "Відображувана назва",
"settings.providers.page.custom.field.name.placeholder": "Мій провайдер",
"settings.providers.page.custom.field.name.info": "Показується у виборі провайдера та моделі.",
"settings.providers.page.custom.field.protocol.label": "Протокол API",
"settings.providers.page.custom.field.protocol.info": "Виберіть формат запиту, який реалізує цей API.",
"settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions",
"settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses",
"settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages",
"settings.providers.page.custom.field.baseURL.label": "Базова URL-адреса",
"settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1",
"settings.providers.page.custom.field.baseURL.info": "Базова URL-адреса OpenAI-сумісного API. Має починатися з http:// або https://.",
@@ -2133,4 +2152,5 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...thirdPartyIntegrationI18n.uk,
} as const;
+70 -14
View File
@@ -998,6 +998,8 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.draft": "Чернетка",
"gitView.pr.field.title": "Назва",
"gitView.pr.githubNotConnected": "GitHub не підключено",
"gitView.pr.history.merged": "PR #{number} злито в {base}.",
"gitView.pr.history.closed": "PR #{number} закрито.",
"gitView.pr.loadingDescription": "Завантаження опису...",
"gitView.pr.mergeMethod.merge": "Створити коміт злиття",
"gitView.pr.mergeMethod.rebase": "Перебазувати та злити",
@@ -1187,12 +1189,12 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "Small model не підтримує структуровані відповіді, потрібні для розбору.",
"contextRail.surface.plan.description": "Перегляд поточного плану",
"contextRail.surface.pr.description": "Створюйте, переглядайте та зливайте pull request поточної гілки",
"contextRail.surface.notes.description": "Нотатки, задачі та плани проєкту",
"contextRail.surface.notes.description": "Нотатки, завдання, плани та памʼять агента для проєкту",
"contextRail.surface.context.description": "Контекст сесії та використання токенів",
"contextRail.surface.browser.description": "Вбудований браузер",
"contextRail.surface.preview.description": "Перегляд дев-сервера",
"contextRail.surface.chat.description": "Сесія, відкрита поруч",
"contextRail.surface.notes": "Нотатки проєкту",
"contextRail.surface.notes": "Знання проєкту",
"contextRail.editorTree.toggle": "Перемкнути дерево файлів",
"contextPanel.browser.open": "Відкрити панель браузера",
"contextPanel.browser.addressAria": "Адреса браузера",
@@ -1286,6 +1288,14 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.toast.writeNotSupported": "Запис не підтримується",
"sidebarFilesTree.toast.fileCreated": "Файл створено",
"sidebarFilesTree.toast.operationFailed": "Операція не вдалася",
"sidebarFilesTree.toast.uploaded": "Файли завантажено",
"sidebarFilesTree.toast.uploadedWithoutConflicts": "Файли без конфліктів завантажено",
"sidebarFilesTree.toast.uploadFailed": "Деякі файли не вдалося завантажити",
"sidebarFilesTree.drop.target": "Завантажити в {path}",
"sidebarFilesTree.drop.uploading": "Завантаження файлів у {path}",
"sidebarFilesTree.dialog.uploadConflicts.title": "Замінити наявні файли?",
"sidebarFilesTree.dialog.uploadConflicts.description": "Файли з такими назвами вже існують у {path}. Заміну неможливо скасувати.",
"sidebarFilesTree.dialog.uploadConflicts.replace": "Замінити",
"sidebarFilesTree.toast.folderNameRequired": "Потрібно вказати назву папки",
"sidebarFilesTree.toast.folderCreated": "Папку створено",
"sidebarFilesTree.toast.nameRequired": "Потрібно вказати назву",
@@ -1393,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
"contextUsage.mobile.usedTokens": "Використані токени",
"contextUsage.mobile.contextLimit": "Обмеження контексту",
"contextUsage.mobile.outputLimit": "Ліміт виводу",
"contextUsage.mobile.cost": "Вартість",
"contextUsage.mobile.usage": "Використання",
"contextUsage.tooltip.usedTokens": "Використані токени: {tokens}",
"contextUsage.tooltip.contextLimit": "Обмеження контексту: {tokens}",
"contextUsage.tooltip.outputLimit": "Ліміт виводу: {tokens}",
"contextUsage.tooltip.cost": "Вартість: {cost}",
"contextSidebar.session.untitled": "Сесія без назви",
"contextSidebar.empty.openSession": "Відкрийте сесію, щоб перевірити контекст.",
"contextSidebar.section.context": "Контекст",
@@ -1423,6 +1435,7 @@ export const dict: Record<I18nKey, string> = {
"planView.file.defaultName": "план",
"planView.title.default": "План",
"planView.error.saveFailed": "Не вдалося зберегти",
"planView.error.loadFailed": "Не вдалося завантажити цей план",
"planView.error.previewUnavailable": "Попередній перегляд недоступний",
"planView.error.switchToEditMode": "Перейдіть у режим редагування, щоб усунути проблему.",
"planView.error.writeFailed": "Помилка запису",
@@ -1517,11 +1530,46 @@ export const dict: Record<I18nKey, string> = {
"diffView.hunk.unsupported": "Додавання окремих шматків до індексу не підтримується в цьому середовищі.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "План",
"rightSidebar.contextNotesTodo.empty.selectProject": "Виберіть проєкт, щоб додати нотатки та завдання.",
"rightSidebar.contextNotesTodo.notes.title": "Швидкі нотатки - {project}",
"rightSidebar.contextNotesTodo.notes.placeholder": "Зберігайте контекст, нагадування або посилання",
"rightSidebar.contextNotesTodo.todo.title": "Todo",
"rightSidebar.contextNotesTodo.todo.itemsSingle": "{count} пункт",
"rightSidebar.contextNotesTodo.todo.itemsPlural": "пунктів: {count}",
"rightSidebar.contextNotesTodo.notes.addAria": "Додати нотатку",
"rightSidebar.contextNotesTodo.notes.empty": "Нотаток ще немає. Занотуйте контекст, нагадування або посилання.",
"rightSidebar.contextNotesTodo.notes.actions.expand": "Розгорнути нотатку",
"rightSidebar.contextNotesTodo.notes.actions.collapse": "Згорнути нотатку",
"rightSidebar.contextNotesTodo.notes.actions.delete": "Видалити нотатку",
"rightSidebar.contextNotesTodo.notes.actions.pin": "Закріпити в контексті агента",
"rightSidebar.contextNotesTodo.notes.actions.unpin": "Відкріпити з контексту агента",
"rightSidebar.contextNotesTodo.notes.source.selection": "З чату",
"rightSidebar.contextNotesTodo.notes.source.agent": "Від агента",
"rightSidebar.contextNotesTodo.search.placeholder": "Пошук",
"rightSidebar.contextNotesTodo.search.clear": "Очистити пошук",
"rightSidebar.contextNotesTodo.search.noResults": "Нічого не знайдено за запитом «{query}».",
"rightSidebar.contextNotesTodo.toast.deleteNoteFailed": "Не вдалося видалити нотатку",
"rightSidebar.contextNotesTodo.toast.createNoteFailed": "Не вдалося створити нотатку",
"rightSidebar.contextNotesTodo.tabs.notes": "Нотатки",
"rightSidebar.contextNotesTodo.tabs.todos": "Todo",
"rightSidebar.contextNotesTodo.tabs.plans": "Плани",
"rightSidebar.contextNotesTodo.plans.actions.back": "Назад до планів",
"rightSidebar.contextNotesTodo.tabs.memory": "Памʼять",
"rightSidebar.contextNotesTodo.sections.label": "Розділи контексту проєкту",
"rightSidebar.contextNotesTodo.sections.resize": "Змінити ширину бічної панелі розділів",
"rightSidebar.contextNotesTodo.memory.scope.project": "Проєкт",
"rightSidebar.contextNotesTodo.memory.scope.label": "Область памʼяті",
"rightSidebar.contextNotesTodo.memory.scope.global": "Про вас",
"rightSidebar.contextNotesTodo.memory.type.fact": "факт",
"rightSidebar.contextNotesTodo.memory.badge.new": "нове",
"rightSidebar.contextNotesTodo.memory.flagged": "Не надсилається агенту — виглядає як інструкція",
"rightSidebar.contextNotesTodo.memory.badge.changed": "змінено",
"rightSidebar.contextNotesTodo.memory.type.preference": "вподобання",
"rightSidebar.contextNotesTodo.memory.type.reference": "посилання",
"rightSidebar.contextNotesTodo.memory.actions.delete": "Забути цей запис",
"rightSidebar.contextNotesTodo.memory.actions.editTitle": "Заголовок запису",
"rightSidebar.contextNotesTodo.memory.actions.editBody": "Текст запису",
"rightSidebar.contextNotesTodo.memory.toast.saveFailed": "Не вдалося зберегти запис",
"rightSidebar.contextNotesTodo.memory.toast.deleteFailed": "Не вдалося забути запис",
"rightSidebar.contextNotesTodo.memory.empty.nothing": "Агент ще нічого сюди не записав.",
"rightSidebar.contextNotesTodo.memory.empty.noMatches": "Жоден збережений запис не відповідає пошуку.",
"rightSidebar.contextNotesTodo.memory.empty.noProject": "Відкрийте проєкт, щоб побачити, що агент про нього памʼятає.",
"rightSidebar.contextNotesTodo.memory.empty.unavailable": "Не вдалося завантажити памʼять. Нічого не втрачено — спробуйте ще раз.",
"rightSidebar.contextNotesTodo.todo.clearCompleted": "Очистити завершені",
"rightSidebar.contextNotesTodo.todo.inputPlaceholder": "Додати завдання",
"rightSidebar.contextNotesTodo.todo.addAria": "Додати завдання",
@@ -1532,13 +1580,9 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.todo.actions.delete": "Видалити \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.send": "Надіслати \"{text}\"",
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Змінити порядок \"{text}\"",
"rightSidebar.contextNotesTodo.todo.resizeAria": "Змінити розмір списку завдань",
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Надіслати до поточної сесії",
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Надіслати до нової сесії",
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Надіслати до нової сесії в worktree",
"rightSidebar.contextNotesTodo.plans.title": "Плани",
"rightSidebar.contextNotesTodo.plans.filesSingle": "Файл: {count}",
"rightSidebar.contextNotesTodo.plans.filesPlural": "Файлів: {count}",
"rightSidebar.contextNotesTodo.plans.importFromFile": "Імпортувати план із файлу",
"rightSidebar.contextNotesTodo.plans.empty": "Ще немає збережених планів.",
"rightSidebar.contextNotesTodo.plans.deletePlan": "Видалити план",
@@ -1558,6 +1602,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.sentToNewSession": "Завдання надіслано до нової сесії",
"rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession": "Завдання надіслано до нової сесії в worktree",
"rightSidebar.contextNotesTodo.toast.sendTodoFailed": "Не вдалося надіслати завдання",
"rightSidebar.contextNotesTodo.toast.updatePlanFailed": "Не вдалося оновити план",
"rightSidebar.contextNotesTodo.toast.deletePlanFailed": "Не вдалося видалити план",
"rightSidebar.contextNotesTodo.toast.planFileEmpty": "Файл плану порожній",
"rightSidebar.contextNotesTodo.toast.importPlanFailed": "Не вдалося імпортувати план",
@@ -1993,6 +2038,9 @@ export const dict: Record<I18nKey, string> = {
"chat.revert.toast.undo": "Відкочено до {preview}",
"chat.revert.toast.redo": "Повторено",
"chat.revert.toast.restored": "Всі повідомлення відновлено",
"chat.toast.opencodeRestartInterrupted.title": "Чат перервано",
"chat.toast.opencodeRestartInterrupted.description": "OpenCode перезапустився, поки відповідь ще формувалася. Надішліть повідомлення, щоб продовжити.",
"chat.toast.opencodeRestartInterrupted.openSession": "Відкрити сесію",
"chat.errorBoundary.title": "Помилка чату",
"chat.errorBoundary.description": "В інтерфейсі чату сталася помилка. Причиною може бути тимчасова проблема з мережею або пошкоджені дані повідомлення.",
"chat.errorBoundary.sessionLabel": "Сесія",
@@ -2983,13 +3031,15 @@ export const dict: Record<I18nKey, string> = {
"contextFileOpen.failure.unreadable": "Failed to open file",
"quota.window.5h": "5-Hour",
"quota.window.7d": "7-Day Limit",
"quota.window.7dSonnet": "7-Day Sonnet Limit",
"quota.window.7dOpus": "7-Day Opus Limit",
"quota.window.weekly": "Weekly Limit",
"quota.window.extraUsage": "Додаткове використання",
"quota.window.weekly": "Щотижня",
"quota.window.daily": "Daily",
"quota.window.monthly": "Monthly Limit",
"quota.window.monthly": "Щомісяця",
"quota.window.credits": "Credits",
"quota.window.creditsBalance": "Credits Balance",
"quota.window.monthlyCredits": "Місячні кредити",
"quota.window.purchasedCredits": "Придбані кредити",
"quota.window.freeCredits": "Безкоштовні кредити",
"quota.window.billingCycle": "Billing Cycle",
"quota.window.auto": "Auto",
"quota.window.api": "API",
@@ -3032,6 +3082,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': 'поставив питання',
'chat.workStatus.section.contextBreakdown': 'Джерела контексту',
'chat.workStatus.breakdown.skills': 'Скіли',
'chat.workStatus.breakdown.pinnedNote': 'нотатка',
'chat.workStatus.breakdown.unpin': 'Відкріпити від контексту',
'chat.workStatus.breakdown.pinnedPlan': 'план',
'chat.workStatus.breakdown.memory': 'Памʼять агента',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '{count} закріплено',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '{count} закріплено',
'chat.workStatus.breakdown.mcp': 'Сервери MCP',
'chat.workStatus.action.openChanges': 'Відкрити зміни',
'chat.workStatus.action.openGit': 'Відкрити панель Git',
@@ -1,3 +1,4 @@
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 用量跟踪',
'settings.providers.page.openCodeGo.description': '连接 OpenCode Go 控制面板以显示滚动、每周和每月配额。',
@@ -864,16 +865,26 @@ export const settingsDict = {
'settings.skills.catalog.page.mode.manual': '手动',
'settings.skills.catalog.page.mode.external': '外部',
'settings.skills.catalog.page.title': '技能目录',
'settings.skills.catalog.page.subtitle': '从精选仓库安装现成技能,或添加你自己的来源。',
'settings.skills.catalog.page.section.sources': '来源',
'settings.skills.catalog.page.searchAllPlaceholder': '在所有来源中搜索技能…',
'settings.skills.catalog.page.search.clear': '清除搜索',
'settings.skills.catalog.page.source.skillsCount': '技能数:{count}',
'settings.skills.catalog.page.source.stars': '星标:{count}',
'settings.skills.catalog.page.source.updated': '更新于 {time}',
'settings.skills.catalog.page.source.addOwnTitle': '添加自己的来源',
'settings.skills.catalog.page.source.addOwnDescription': '任何包含技能的 Git 仓库',
'settings.skills.catalog.page.source.viewRepo': '在 GitHub 上打开仓库',
'settings.skills.catalog.page.skill.viewOnGithub': '在 GitHub 上查看技能',
'settings.skills.catalog.page.list.searchTitle': '搜索结果',
'settings.skills.catalog.page.section.sourceRepository': '来源仓库',
'settings.skills.catalog.page.field.selectSourcePlaceholder': '选择来源',
'settings.skills.catalog.page.actions.refreshTitle': '刷新',
'settings.skills.catalog.page.actions.removeCatalogTitle': '移除目录',
'settings.skills.catalog.page.actions.addCatalog': '添加目录',
'settings.skills.catalog.page.actions.removeCatalog': '移除目录',
'settings.skills.catalog.page.actions.loadMoreSkills': '加载更多技能',
'settings.skills.catalog.page.loading.catalog': '加载中...',
'settings.skills.catalog.page.loading.skills': '正在加载技能...',
'settings.skills.catalog.page.loading.more': '加载中...',
'settings.skills.catalog.page.foundCount': '找到 {count} 个技能',
'settings.skills.catalog.page.error.catalogTitle': '目录错误',
'settings.skills.catalog.page.empty.noSkillsTitle': '未找到技能',
@@ -881,7 +892,6 @@ export const settingsDict = {
'settings.skills.catalog.page.badge.installed': '已安装({scope}',
'settings.skills.catalog.page.badge.notInstallable': '不可安装',
'settings.skills.catalog.page.badge.unknown': '未知',
'settings.skills.catalog.page.byOwnerPrefix': '作者',
'settings.skills.catalog.page.removeDialog.title': '移除目录',
'settings.skills.catalog.page.removeDialog.description': '确定要移除此目录吗?',
'settings.openchamber.passkeys.title': 'Passkeys',
@@ -980,6 +990,9 @@ export const settingsDict = {
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber Web 工具',
'settings.openchamber.tools.field.agentWebToolAria': '启用 OpenChamber Web 工具',
'settings.openchamber.tools.field.agentWebToolInfo': '让智能体在 OpenChamber 浏览器面板中查看并操作页面:打开网址、读取内容、点击、输入、滚动,以及在移动端与桌面端布局之间切换。会为每个会话添加少量工具说明。在 OpenCode 重启后生效。',
'settings.openchamber.tools.field.agentMemoryTool': '智能体记忆工具',
'settings.openchamber.tools.field.agentMemoryToolAria': '智能体记忆工具',
'settings.openchamber.tools.field.agentMemoryToolInfo': '让智能体把学到的内容跨会话保留下来,分为两个存储:关于你的事实,以及关于每个项目的事实。会话会收到已存条目的标题,智能体可在相关时读取具体内容。关闭后将同时移除该工具、记忆标签页和会话索引。重启 OpenCode 后生效。',
'settings.openchamber.opencodeCli.tooltipPrefix': '可选的',
'settings.openchamber.opencodeCli.tooltipSuffix': '二进制绝对路径。',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode 可执行文件路径',
@@ -1160,6 +1173,7 @@ export const settingsDict = {
'settings.usage.page.header.providerUsage': '{provider} 用量',
'settings.usage.page.header.refreshing': '正在刷新用量...',
'settings.usage.page.header.lastUpdated': '最后更新:{time}',
'settings.usage.page.header.lastUpdatedWithPlan': '套餐:{plan} · 最后更新:{time}',
'settings.usage.page.options.showInWorkStatusAria': '在工作状态面板中显示',
'settings.usage.page.options.showInWorkStatus': '在工作状态面板中显示',
'settings.usage.page.options.showInWorkStatusTooltip': '启用后,该提供商的用量会显示在工作状态面板中。',
@@ -1326,13 +1340,18 @@ export const settingsDict = {
'settings.providers.page.custom.title': '自定义提供商',
'settings.providers.page.custom.editTitle': '编辑自定义提供商',
'settings.providers.page.custom.description': '通过指定基础 URL、凭据模型列表,添加兼容 OpenAI 的提供商。会写入 OpenCode 配置,可像其他提供商一样在聊天使用。',
'settings.providers.page.custom.description': '通过指定基础 URL、凭据模型列表和支持的 API 协议添加提供商。会写入 OpenCode 配置以供聊天使用。',
'settings.providers.page.custom.field.providerID.label': '提供商 ID',
'settings.providers.page.custom.field.providerID.placeholder': 'my-provider',
'settings.providers.page.custom.field.providerID.info': '小写字母、数字、连字符和下划线。用作 OpenCode 提供商 ID。',
'settings.providers.page.custom.field.name.label': '显示名称',
'settings.providers.page.custom.field.name.placeholder': '我的提供商',
'settings.providers.page.custom.field.name.info': '显示在提供商和模型选择器中。',
'settings.providers.page.custom.field.protocol.label': 'API 协议',
'settings.providers.page.custom.field.protocol.info': '选择此 API 实现的请求格式。',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': '基础 URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': '兼容 OpenAI 的 API 基础 URL。必须以 http:// 或 https:// 开头。',
@@ -2133,4 +2152,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n['zh-CN'],
} as const;
+70 -14
View File
@@ -998,6 +998,8 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.draft': '草稿',
'gitView.pr.field.title': '标题',
'gitView.pr.githubNotConnected': 'GitHub 未连接',
'gitView.pr.history.merged': 'PR #{number} 已合并到 {base}。',
'gitView.pr.history.closed': 'PR #{number} 已关闭。',
'gitView.pr.loadingDescription': '正在加载描述...',
'gitView.pr.mergeMethod.merge': '创建合并提交',
'gitView.pr.mergeMethod.rebase': '变基并合并',
@@ -1187,12 +1189,12 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '小模型不支持导读所需的结构化响应。',
'contextRail.surface.plan.description': '查看当前计划',
'contextRail.surface.pr.description': '创建、审查并合并当前分支的拉取请求',
'contextRail.surface.notes.description': '项目的笔记、待办计划',
'contextRail.surface.notes.description': '项目的笔记、待办计划和智能体记忆',
'contextRail.surface.context.description': '会话上下文与令牌用量',
'contextRail.surface.browser.description': '内置网页浏览器',
'contextRail.surface.preview.description': '开发服务器预览',
'contextRail.surface.chat.description': '并排打开的会话',
'contextRail.surface.notes': '项目笔记',
'contextRail.surface.notes': '项目知识',
'contextRail.editorTree.toggle': '切换文件树',
'contextPanel.browser.open': '打开浏览器面板',
'contextPanel.browser.addressAria': '浏览器地址',
@@ -1286,6 +1288,14 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.toast.writeNotSupported': '不支持写入',
'sidebarFilesTree.toast.fileCreated': '文件已创建',
'sidebarFilesTree.toast.operationFailed': '操作失败',
'sidebarFilesTree.toast.uploaded': '文件已上传',
'sidebarFilesTree.toast.uploadedWithoutConflicts': '无冲突的文件已上传',
'sidebarFilesTree.toast.uploadFailed': '部分文件无法上传',
'sidebarFilesTree.drop.target': '上传到 {path}',
'sidebarFilesTree.drop.uploading': '正在将文件上传到 {path}',
'sidebarFilesTree.dialog.uploadConflicts.title': '替换现有文件?',
'sidebarFilesTree.dialog.uploadConflicts.description': '{path} 中已存在同名文件。替换后无法撤销。',
'sidebarFilesTree.dialog.uploadConflicts.replace': '替换',
'sidebarFilesTree.toast.folderNameRequired': '文件夹名不能为空',
'sidebarFilesTree.toast.folderCreated': '文件夹已创建',
'sidebarFilesTree.toast.nameRequired': '名称不能为空',
@@ -1393,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
'contextUsage.mobile.usedTokens': '已用 Token',
'contextUsage.mobile.contextLimit': '上下文上限',
'contextUsage.mobile.outputLimit': '输出上限',
'contextUsage.mobile.cost': '成本',
'contextUsage.mobile.usage': '使用率',
'contextUsage.tooltip.usedTokens': '已用 Token{tokens}',
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
'contextUsage.tooltip.outputLimit': '输出上限:{tokens}',
'contextUsage.tooltip.cost': '成本:{cost}',
'contextSidebar.session.untitled': '未命名会话',
'contextSidebar.empty.openSession': '请先打开会话以查看上下文。',
'contextSidebar.section.context': '上下文',
@@ -1423,6 +1435,7 @@ export const dict: Record<I18nKey, string> = {
'planView.file.defaultName': 'plan',
'planView.title.default': '计划',
'planView.error.saveFailed': '保存失败',
'planView.error.loadFailed': '无法加载此计划',
'planView.error.previewUnavailable': '预览不可用',
'planView.error.switchToEditMode': '请切换到编辑模式修复问题。',
'planView.error.writeFailed': '写入失败',
@@ -1505,11 +1518,46 @@ export const dict: Record<I18nKey, string> = {
'diffView.hunk.unsupported': '此运行环境不支持暂存单个代码块。',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '计划',
'rightSidebar.contextNotesTodo.empty.selectProject': '请选择一个项目以添加笔记和待办事项。',
'rightSidebar.contextNotesTodo.notes.title': '快速笔记 - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': '记录上下文、提醒或链接',
'rightSidebar.contextNotesTodo.todo.title': '待办',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count} 项',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count} 项',
'rightSidebar.contextNotesTodo.notes.addAria': '添加笔记',
'rightSidebar.contextNotesTodo.notes.empty': '还没有笔记。可以记录上下文、提醒或链接。',
'rightSidebar.contextNotesTodo.notes.actions.expand': '展开笔记',
'rightSidebar.contextNotesTodo.notes.actions.collapse': '折叠笔记',
'rightSidebar.contextNotesTodo.notes.actions.delete': '删除笔记',
'rightSidebar.contextNotesTodo.notes.actions.pin': '固定到智能体上下文',
'rightSidebar.contextNotesTodo.notes.actions.unpin': '从智能体上下文取消固定',
'rightSidebar.contextNotesTodo.notes.source.selection': '来自对话',
'rightSidebar.contextNotesTodo.notes.source.agent': '来自智能体',
'rightSidebar.contextNotesTodo.search.placeholder': '搜索',
'rightSidebar.contextNotesTodo.search.clear': '清除搜索',
'rightSidebar.contextNotesTodo.search.noResults': '没有匹配 "{query}" 的内容。',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': '删除笔记失败',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': '创建笔记失败',
'rightSidebar.contextNotesTodo.tabs.notes': '笔记',
'rightSidebar.contextNotesTodo.tabs.todos': '待办',
'rightSidebar.contextNotesTodo.tabs.plans': '计划',
'rightSidebar.contextNotesTodo.plans.actions.back': '返回计划列表',
'rightSidebar.contextNotesTodo.tabs.memory': '记忆',
'rightSidebar.contextNotesTodo.sections.label': '项目上下文分区',
'rightSidebar.contextNotesTodo.sections.resize': '调整分区侧栏宽度',
'rightSidebar.contextNotesTodo.memory.scope.project': '项目',
'rightSidebar.contextNotesTodo.memory.scope.label': '记忆范围',
'rightSidebar.contextNotesTodo.memory.scope.global': '关于你',
'rightSidebar.contextNotesTodo.memory.type.fact': '事实',
'rightSidebar.contextNotesTodo.memory.badge.new': '新增',
'rightSidebar.contextNotesTodo.memory.flagged': '不会发送给智能体 — 读起来像指令',
'rightSidebar.contextNotesTodo.memory.badge.changed': '已更改',
'rightSidebar.contextNotesTodo.memory.type.preference': '偏好',
'rightSidebar.contextNotesTodo.memory.type.reference': '参考',
'rightSidebar.contextNotesTodo.memory.actions.delete': '删除这条记忆',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': '记忆标题',
'rightSidebar.contextNotesTodo.memory.actions.editBody': '记忆内容',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': '保存记忆失败',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': '删除记忆失败',
'rightSidebar.contextNotesTodo.memory.empty.nothing': '智能体还没有在这里存过内容。',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': '没有匹配搜索的已存记忆。',
'rightSidebar.contextNotesTodo.memory.empty.noProject': '打开一个项目,查看智能体记住了什么。',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': '无法加载已存记忆。内容并未丢失,请重试。',
'rightSidebar.contextNotesTodo.todo.clearCompleted': '清除已完成',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': '添加待办',
'rightSidebar.contextNotesTodo.todo.addAria': '添加待办',
@@ -1520,13 +1568,9 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.todo.actions.delete': '删除“{text}”',
'rightSidebar.contextNotesTodo.todo.actions.send': '发送“{text}”',
'rightSidebar.contextNotesTodo.todo.actions.reorder': '重新排序"{text}"',
'rightSidebar.contextNotesTodo.todo.resizeAria': '调整待办列表大小',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '发送到当前会话',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '发送到新会话',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '发送到新工作树会话',
'rightSidebar.contextNotesTodo.plans.title': '计划',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} 个文件',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} 个文件',
'rightSidebar.contextNotesTodo.plans.importFromFile': '从文件导入计划',
'rightSidebar.contextNotesTodo.plans.empty': '还没有已保存的计划。',
'rightSidebar.contextNotesTodo.plans.deletePlan': '删除计划',
@@ -1546,6 +1590,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': '待办已发送到新会话',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': '待办已发送到新的工作树会话',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': '发送待办失败',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': '更新计划失败',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': '删除计划失败',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': '计划文件为空',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': '导入计划失败',
@@ -1981,6 +2026,9 @@ export const dict: Record<I18nKey, string> = {
'chat.revert.toast.undo': '已撤回至 {preview}',
'chat.revert.toast.redo': '已重做',
'chat.revert.toast.restored': '已恢复全部消息',
'chat.toast.opencodeRestartInterrupted.title': '聊天已中断',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode 在回复仍在生成时重启了。发送一条消息以继续。',
'chat.toast.opencodeRestartInterrupted.openSession': '打开会话',
'chat.errorBoundary.title': '聊天错误',
'chat.errorBoundary.description': '聊天界面发生错误,可能是临时网络问题或消息数据损坏导致。',
'chat.errorBoundary.sessionLabel': '会话',
@@ -2983,13 +3031,15 @@ export const dict: Record<I18nKey, string> = {
'contextFileOpen.failure.unreadable': 'Failed to open file',
'quota.window.5h': '5-Hour',
'quota.window.7d': '7-Day Limit',
'quota.window.7dSonnet': '7-Day Sonnet Limit',
'quota.window.7dOpus': '7-Day Opus Limit',
'quota.window.weekly': 'Weekly Limit',
'quota.window.extraUsage': '额外用量',
'quota.window.weekly': '每周',
'quota.window.daily': 'Daily',
'quota.window.monthly': 'Monthly Limit',
'quota.window.monthly': '每月',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Credits Balance',
'quota.window.monthlyCredits': '每月积分',
'quota.window.purchasedCredits': '已购买积分',
'quota.window.freeCredits': '免费积分',
'quota.window.billingCycle': 'Billing Cycle',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3032,6 +3082,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': '提出了问题',
'chat.workStatus.section.contextBreakdown': '上下文来源',
'chat.workStatus.breakdown.skills': '技能',
'chat.workStatus.breakdown.pinnedNote': '笔记',
'chat.workStatus.breakdown.unpin': '从上下文取消固定',
'chat.workStatus.breakdown.pinnedPlan': '计划',
'chat.workStatus.breakdown.memory': '智能体记忆',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '已固定 {count}',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '已固定 {count}',
'chat.workStatus.breakdown.mcp': 'MCP 服务器',
'chat.workStatus.action.openChanges': '打开更改',
'chat.workStatus.action.openGit': '打开 Git 面板',
@@ -1,4 +1,5 @@
export const settingsDict = {
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 用量追蹤',
'settings.providers.page.openCodeGo.description': '連接 OpenCode Go 控制面板以顯示滾動、每週和每月配額。',
'settings.providers.page.openCodeGo.workspaceId': '工作區 ID',
@@ -861,16 +862,26 @@
'settings.skills.catalog.page.mode.manual': '手動',
'settings.skills.catalog.page.mode.external': '外部',
'settings.skills.catalog.page.title': 'Skills 目錄',
'settings.skills.catalog.page.subtitle': '從精選儲存庫安裝現成技能,或新增你自己的來源。',
'settings.skills.catalog.page.section.sources': '來源',
'settings.skills.catalog.page.searchAllPlaceholder': '在所有來源中搜尋技能…',
'settings.skills.catalog.page.search.clear': '清除搜尋',
'settings.skills.catalog.page.source.skillsCount': '技能數:{count}',
'settings.skills.catalog.page.source.stars': '星標:{count}',
'settings.skills.catalog.page.source.updated': '更新於 {time}',
'settings.skills.catalog.page.source.addOwnTitle': '新增自己的來源',
'settings.skills.catalog.page.source.addOwnDescription': '任何包含技能的 Git 儲存庫',
'settings.skills.catalog.page.source.viewRepo': '在 GitHub 上開啟儲存庫',
'settings.skills.catalog.page.skill.viewOnGithub': '在 GitHub 上檢視技能',
'settings.skills.catalog.page.list.searchTitle': '搜尋結果',
'settings.skills.catalog.page.section.sourceRepository': '來源儲存庫',
'settings.skills.catalog.page.field.selectSourcePlaceholder': '選擇來源',
'settings.skills.catalog.page.actions.refreshTitle': '重新整理',
'settings.skills.catalog.page.actions.removeCatalogTitle': '移除目錄',
'settings.skills.catalog.page.actions.addCatalog': '新增目錄',
'settings.skills.catalog.page.actions.removeCatalog': '移除目錄',
'settings.skills.catalog.page.actions.loadMoreSkills': '載入更多 Skills',
'settings.skills.catalog.page.loading.catalog': '載入中...',
'settings.skills.catalog.page.loading.skills': '正在載入 skills...',
'settings.skills.catalog.page.loading.more': '載入中...',
'settings.skills.catalog.page.foundCount': '找到 {count} 個 skill(s)',
'settings.skills.catalog.page.error.catalogTitle': '目錄錯誤',
'settings.skills.catalog.page.empty.noSkillsTitle': '找不到 skills',
@@ -878,7 +889,6 @@
'settings.skills.catalog.page.badge.installed': '已安裝({scope}',
'settings.skills.catalog.page.badge.notInstallable': '不可安裝',
'settings.skills.catalog.page.badge.unknown': '未知',
'settings.skills.catalog.page.byOwnerPrefix': '作者',
'settings.skills.catalog.page.removeDialog.title': '移除目錄',
'settings.skills.catalog.page.removeDialog.description': '確定要移除此目錄嗎?',
'settings.openchamber.passkeys.title': 'Passkeys',
@@ -954,6 +964,9 @@
'settings.openchamber.tools.field.agentWebTool': 'OpenChamber Web 工具',
'settings.openchamber.tools.field.agentWebToolAria': '啟用 OpenChamber Web 工具',
'settings.openchamber.tools.field.agentWebToolInfo': '讓代理在 OpenChamber 瀏覽器面板中檢視並操作頁面:開啟網址、讀取內容、點擊、輸入、捲動,以及在行動版與桌面版版面之間切換。會為每個工作階段加入少量工具說明。在 OpenCode 重新啟動後生效。',
'settings.openchamber.tools.field.agentMemoryTool': '智慧代理記憶工具',
'settings.openchamber.tools.field.agentMemoryToolAria': '智慧代理記憶工具',
'settings.openchamber.tools.field.agentMemoryToolInfo': '讓代理把學到的內容跨工作階段保留下來,分為兩個儲存區:關於你的事實,以及關於每個專案的事實。工作階段會收到已儲存項目的標題,代理可在相關時讀取內容。關閉後會一併移除該工具、記憶分頁與工作階段索引。重新啟動 OpenCode 後生效。',
'settings.openchamber.opencodeCli.tooltipPrefix': '可選的',
'settings.openchamber.opencodeCli.tooltipSuffix': '二進位檔絕對路徑。',
'settings.openchamber.opencodeCli.field.binaryPath': 'OpenCode 可執行檔路徑',
@@ -1067,6 +1080,7 @@
'settings.usage.page.header.providerUsage': '{provider} 用量',
'settings.usage.page.header.refreshing': '正在重新整理用量...',
'settings.usage.page.header.lastUpdated': '最後更新:{time}',
'settings.usage.page.header.lastUpdatedWithPlan': '方案:{plan} · 最後更新:{time}',
'settings.usage.page.options.showInWorkStatusAria': '在工作狀態面板中顯示',
'settings.usage.page.options.showInWorkStatus': '在工作狀態面板中顯示',
'settings.usage.page.options.showInWorkStatusTooltip': '啟用後,該供應商的用量會顯示在工作狀態面板中。',
@@ -1233,13 +1247,18 @@
'settings.providers.page.custom.title': '自訂供應商',
'settings.providers.page.custom.editTitle': '編輯自訂提供者',
'settings.providers.page.custom.description': '透過指定基礎 URL、憑證模型清單,新增相容 OpenAI 的供應商。會寫入 OpenCode 設定,可像其他供應商一樣在聊天使用。',
'settings.providers.page.custom.description': '透過指定基礎 URL、憑證模型清單與支援的 API 通訊協定新增供應商。會寫入 OpenCode 設定以供聊天使用。',
'settings.providers.page.custom.field.providerID.label': '供應商 ID',
'settings.providers.page.custom.field.providerID.placeholder': 'my-provider',
'settings.providers.page.custom.field.providerID.info': '小寫字母、數字、連字號與底線。用作 OpenCode 供應商 ID。',
'settings.providers.page.custom.field.name.label': '顯示名稱',
'settings.providers.page.custom.field.name.placeholder': '我的供應商',
'settings.providers.page.custom.field.name.info': '顯示於供應商與模型選擇器。',
'settings.providers.page.custom.field.protocol.label': 'API 通訊協定',
'settings.providers.page.custom.field.protocol.info': '選擇此 API 實作的請求格式。',
'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions',
'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses',
'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages',
'settings.providers.page.custom.field.baseURL.label': '基礎 URL',
'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1',
'settings.providers.page.custom.field.baseURL.info': '相容 OpenAI 的 API 基礎 URL。必須以 http:// 或 https:// 開頭。',
@@ -2133,4 +2152,5 @@
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...thirdPartyIntegrationI18n['zh-TW'],
} as const;
+70 -14
View File
@@ -1010,6 +1010,8 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.draft': '草稿',
'gitView.pr.field.title': '標題',
'gitView.pr.githubNotConnected': 'GitHub 未連線',
'gitView.pr.history.merged': 'PR #{number} 已合併到 {base}。',
'gitView.pr.history.closed': 'PR #{number} 已關閉。',
'gitView.pr.loadingDescription': '正在載入描述...',
'gitView.pr.mergeMethod.merge': '建立合併提交',
'gitView.pr.mergeMethod.rebase': 'Rebase 並合併',
@@ -1199,12 +1201,12 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '小模型不支援導讀所需的結構化回應。',
'contextRail.surface.plan.description': '檢視目前計畫',
'contextRail.surface.pr.description': '建立、審查並合併目前分支的提取請求',
'contextRail.surface.notes.description': '專案的筆記、待辦計畫',
'contextRail.surface.notes.description': '專案的筆記、待辦計畫與代理記憶',
'contextRail.surface.context.description': '工作階段情境與權杖用量',
'contextRail.surface.browser.description': '內建網頁瀏覽器',
'contextRail.surface.preview.description': '開發伺服器預覽',
'contextRail.surface.chat.description': '並排開啟的工作階段',
'contextRail.surface.notes': '專案筆記',
'contextRail.surface.notes': '專案知識',
'contextRail.editorTree.toggle': '切換檔案樹',
'contextPanel.browser.open': '開啟瀏覽器面板',
'contextPanel.browser.addressAria': '瀏覽器網址',
@@ -1298,6 +1300,14 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.toast.writeNotSupported': '不支援寫入',
'sidebarFilesTree.toast.fileCreated': '檔案已建立',
'sidebarFilesTree.toast.operationFailed': '操作失敗',
'sidebarFilesTree.toast.uploaded': '檔案已上傳',
'sidebarFilesTree.toast.uploadedWithoutConflicts': '無衝突的檔案已上傳',
'sidebarFilesTree.toast.uploadFailed': '部分檔案無法上傳',
'sidebarFilesTree.drop.target': '上傳至 {path}',
'sidebarFilesTree.drop.uploading': '正在將檔案上傳至 {path}',
'sidebarFilesTree.dialog.uploadConflicts.title': '取代現有檔案?',
'sidebarFilesTree.dialog.uploadConflicts.description': '{path} 中已有同名檔案。取代後無法復原。',
'sidebarFilesTree.dialog.uploadConflicts.replace': '取代',
'sidebarFilesTree.toast.folderNameRequired': '資料夾名稱不能為空',
'sidebarFilesTree.toast.folderCreated': '資料夾已建立',
'sidebarFilesTree.toast.nameRequired': '名稱不能為空',
@@ -1403,10 +1413,12 @@ export const dict: Record<I18nKey, string> = {
'contextUsage.mobile.usedTokens': '已用 Token',
'contextUsage.mobile.contextLimit': '上下文上限',
'contextUsage.mobile.outputLimit': '輸出上限',
'contextUsage.mobile.cost': '成本',
'contextUsage.mobile.usage': '使用率',
'contextUsage.tooltip.usedTokens': '已用 Token{tokens}',
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
'contextUsage.tooltip.outputLimit': '輸出上限:{tokens}',
'contextUsage.tooltip.cost': '成本:{cost}',
'contextSidebar.session.untitled': '未命名會話',
'contextSidebar.empty.openSession': '請先開啟會話以查看上下文。',
'contextSidebar.section.context': '上下文',
@@ -1433,6 +1445,7 @@ export const dict: Record<I18nKey, string> = {
'planView.file.defaultName': 'plan',
'planView.title.default': '計畫',
'planView.error.saveFailed': '儲存失敗',
'planView.error.loadFailed': '無法載入此計畫',
'planView.error.previewUnavailable': '預覽無法使用',
'planView.error.switchToEditMode': '請切換到編輯模式修復問題。',
'planView.error.writeFailed': '寫入失敗',
@@ -1515,11 +1528,46 @@ export const dict: Record<I18nKey, string> = {
'diffView.hunk.unsupported': '此執行環境不支援暫存個別程式碼區塊。',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '計畫',
'rightSidebar.contextNotesTodo.empty.selectProject': '請選擇一個專案以新增筆記和待辦事項。',
'rightSidebar.contextNotesTodo.notes.title': '快速筆記 - {project}',
'rightSidebar.contextNotesTodo.notes.placeholder': '記錄上下文、提醒或連結',
'rightSidebar.contextNotesTodo.todo.title': '待辦',
'rightSidebar.contextNotesTodo.todo.itemsSingle': '{count} 項',
'rightSidebar.contextNotesTodo.todo.itemsPlural': '{count} 項',
'rightSidebar.contextNotesTodo.notes.addAria': '新增筆記',
'rightSidebar.contextNotesTodo.notes.empty': '尚無筆記。可以記錄脈絡、提醒或連結。',
'rightSidebar.contextNotesTodo.notes.actions.expand': '展開筆記',
'rightSidebar.contextNotesTodo.notes.actions.collapse': '收合筆記',
'rightSidebar.contextNotesTodo.notes.actions.delete': '刪除筆記',
'rightSidebar.contextNotesTodo.notes.actions.pin': '釘選到代理上下文',
'rightSidebar.contextNotesTodo.notes.actions.unpin': '從代理上下文取消釘選',
'rightSidebar.contextNotesTodo.notes.source.selection': '來自對話',
'rightSidebar.contextNotesTodo.notes.source.agent': '來自代理',
'rightSidebar.contextNotesTodo.search.placeholder': '搜尋',
'rightSidebar.contextNotesTodo.search.clear': '清除搜尋',
'rightSidebar.contextNotesTodo.search.noResults': '沒有符合「{query}」的內容。',
'rightSidebar.contextNotesTodo.toast.deleteNoteFailed': '刪除筆記失敗',
'rightSidebar.contextNotesTodo.toast.createNoteFailed': '建立筆記失敗',
'rightSidebar.contextNotesTodo.tabs.notes': '筆記',
'rightSidebar.contextNotesTodo.tabs.todos': '待辦',
'rightSidebar.contextNotesTodo.tabs.plans': '計畫',
'rightSidebar.contextNotesTodo.plans.actions.back': '返回計畫列表',
'rightSidebar.contextNotesTodo.tabs.memory': '記憶',
'rightSidebar.contextNotesTodo.sections.label': '專案脈絡分區',
'rightSidebar.contextNotesTodo.sections.resize': '調整分區側欄寬度',
'rightSidebar.contextNotesTodo.memory.scope.project': '專案',
'rightSidebar.contextNotesTodo.memory.scope.label': '記憶範圍',
'rightSidebar.contextNotesTodo.memory.scope.global': '關於你',
'rightSidebar.contextNotesTodo.memory.type.fact': '事實',
'rightSidebar.contextNotesTodo.memory.badge.new': '新增',
'rightSidebar.contextNotesTodo.memory.flagged': '不會傳給代理 — 讀起來像指令',
'rightSidebar.contextNotesTodo.memory.badge.changed': '已變更',
'rightSidebar.contextNotesTodo.memory.type.preference': '偏好',
'rightSidebar.contextNotesTodo.memory.type.reference': '參考',
'rightSidebar.contextNotesTodo.memory.actions.delete': '刪除這則記憶',
'rightSidebar.contextNotesTodo.memory.actions.editTitle': '記憶標題',
'rightSidebar.contextNotesTodo.memory.actions.editBody': '記憶內容',
'rightSidebar.contextNotesTodo.memory.toast.saveFailed': '儲存記憶失敗',
'rightSidebar.contextNotesTodo.memory.toast.deleteFailed': '刪除記憶失敗',
'rightSidebar.contextNotesTodo.memory.empty.nothing': '代理還沒有在這裡儲存內容。',
'rightSidebar.contextNotesTodo.memory.empty.noMatches': '沒有符合搜尋的已儲存記憶。',
'rightSidebar.contextNotesTodo.memory.empty.noProject': '開啟專案即可查看代理記住了什麼。',
'rightSidebar.contextNotesTodo.memory.empty.unavailable': '無法載入已儲存的記憶。內容並未遺失,請再試一次。',
'rightSidebar.contextNotesTodo.todo.clearCompleted': '清除已完成',
'rightSidebar.contextNotesTodo.todo.inputPlaceholder': '新增待辦',
'rightSidebar.contextNotesTodo.todo.addAria': '新增待辦',
@@ -1530,13 +1578,9 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.todo.actions.delete': '刪除「{text}」',
'rightSidebar.contextNotesTodo.todo.actions.send': '傳送「{text}」',
'rightSidebar.contextNotesTodo.todo.actions.reorder': '重新排序「{text}」',
'rightSidebar.contextNotesTodo.todo.resizeAria': '調整待辦清單大小',
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '傳送到目前會話',
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '傳送到新會話',
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '傳送到新 worktree 會話',
'rightSidebar.contextNotesTodo.plans.title': '計畫',
'rightSidebar.contextNotesTodo.plans.filesSingle': '{count} 個檔案',
'rightSidebar.contextNotesTodo.plans.filesPlural': '{count} 個檔案',
'rightSidebar.contextNotesTodo.plans.importFromFile': '從檔案匯入計畫',
'rightSidebar.contextNotesTodo.plans.empty': '還沒有已儲存的計畫。',
'rightSidebar.contextNotesTodo.plans.deletePlan': '刪除計畫',
@@ -1556,6 +1600,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.sentToNewSession': '待辦已傳送到新會話',
'rightSidebar.contextNotesTodo.toast.sentToNewWorktreeSession': '待辦已傳送到新的 worktree 會話',
'rightSidebar.contextNotesTodo.toast.sendTodoFailed': '傳送待辦失敗',
'rightSidebar.contextNotesTodo.toast.updatePlanFailed': '更新計畫失敗',
'rightSidebar.contextNotesTodo.toast.deletePlanFailed': '刪除計畫失敗',
'rightSidebar.contextNotesTodo.toast.planFileEmpty': '計畫檔案為空',
'rightSidebar.contextNotesTodo.toast.importPlanFailed': '匯入計畫失敗',
@@ -1985,6 +2030,9 @@ export const dict: Record<I18nKey, string> = {
'chat.revert.toast.undo': '已收回至 {preview}',
'chat.revert.toast.redo': '已重做',
'chat.revert.toast.restored': '已恢復全部訊息',
'chat.toast.opencodeRestartInterrupted.title': '聊天已中斷',
'chat.toast.opencodeRestartInterrupted.description': 'OpenCode 在回覆仍在產生時重新啟動。傳送訊息以繼續。',
'chat.toast.opencodeRestartInterrupted.openSession': '開啟會話',
'chat.errorBoundary.title': '聊天錯誤',
'chat.errorBoundary.description': '聊天介面發生錯誤,可能是暫時網路問題或訊息資料損毀導致。',
'chat.errorBoundary.sessionLabel': '會話',
@@ -2982,13 +3030,15 @@ export const dict: Record<I18nKey, string> = {
'contextFileOpen.failure.unreadable': 'Failed to open file',
'quota.window.5h': '5-Hour',
'quota.window.7d': '7-Day Limit',
'quota.window.7dSonnet': '7-Day Sonnet Limit',
'quota.window.7dOpus': '7-Day Opus Limit',
'quota.window.weekly': 'Weekly Limit',
'quota.window.extraUsage': '額外用量',
'quota.window.weekly': '每週',
'quota.window.daily': 'Daily',
'quota.window.monthly': 'Monthly Limit',
'quota.window.monthly': '每月',
'quota.window.credits': 'Credits',
'quota.window.creditsBalance': 'Credits Balance',
'quota.window.monthlyCredits': '每月點數',
'quota.window.purchasedCredits': '已購買點數',
'quota.window.freeCredits': '免費點數',
'quota.window.billingCycle': 'Billing Cycle',
'quota.window.auto': 'Auto',
'quota.window.api': 'API',
@@ -3031,6 +3081,12 @@ export const dict: Record<I18nKey, string> = {
'chat.workStatus.subagent.askedQuestion': '提出了問題',
'chat.workStatus.section.contextBreakdown': '上下文來源',
'chat.workStatus.breakdown.skills': '技能',
'chat.workStatus.breakdown.pinnedNote': '筆記',
'chat.workStatus.breakdown.unpin': '從脈絡取消釘選',
'chat.workStatus.breakdown.pinnedPlan': '計畫',
'chat.workStatus.breakdown.memory': '代理記憶',
'chat.workStatus.breakdown.pinnedKnowledgeSingle': '已釘選 {count}',
'chat.workStatus.breakdown.pinnedKnowledgePlural': '已釘選 {count}',
'chat.workStatus.breakdown.mcp': 'MCP 伺服器',
'chat.workStatus.action.openChanges': '開啟變更',
'chat.workStatus.action.openGit': '開啟 Git 面板',
+19
View File
@@ -0,0 +1,19 @@
import { getCurrentIntlLocale } from './i18n';
export const formatMoney = (value: number): string => {
if (!Number.isFinite(value) || value <= 0) {
return new Intl.NumberFormat(getCurrentIntlLocale(), {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(0);
}
return new Intl.NumberFormat(getCurrentIntlLocale(), {
style: 'currency',
currency: 'USD',
minimumFractionDigits: value < 0.01 ? 4 : 2,
maximumFractionDigits: value < 0.01 ? 4 : 2,
}).format(value);
};
+5 -379
View File
@@ -1,7 +1,11 @@
/**
* OpenChamber project-level configuration service.
* Stores per-project settings in ~/.config/openchamber/<projectId>.json.
* Stores per-project settings in ~/.config/openchamber/projects/<projectId>.json.
* Migrates from legacy <project>/.openchamber/openchamber.json.
*
* Notes, todos, and plan files used to live here too. They are now server-owned
* (`packages/web/server/lib/project-context`) and reached through
* `@/lib/projectContextApi`; what remains here is the client-owned rest.
*/
import type { FilesAPI } from './api/types';
@@ -34,9 +38,6 @@ interface OpenChamberConfig {
projectPath?: string;
'setup-worktree'?: string[];
'setup-worktree-wait'?: boolean;
projectNotes?: string;
projectTodos?: OpenChamberProjectTodoItem[];
projectPlanFiles?: OpenChamberProjectPlanFileLink[];
projectActions?: OpenChamberProjectAction[];
projectActionsPrimaryId?: string;
draftStarters?: DraftStarterRef[];
@@ -60,42 +61,10 @@ export interface OpenChamberProjectActionsState {
primaryActionId: string | null;
}
export interface OpenChamberProjectTodoItem {
id: string;
text: string;
completed: boolean;
createdAt: number;
}
export interface OpenChamberProjectPlanFileLink {
id: string;
path: string;
createdAt: number;
}
export interface OpenChamberProjectPlanFile {
title: string;
body: string;
raw: string;
path: string;
}
export interface OpenChamberProjectNotesTodos {
notes: string;
todos: OpenChamberProjectTodoItem[];
}
export interface OpenChamberProjectContextData extends OpenChamberProjectNotesTodos {
plans: OpenChamberProjectPlanFileLink[];
}
export const OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH = 3000;
export const OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH = 120;
const OPENCHAMBER_PROJECT_ACTION_NAME_MAX_LENGTH = 80;
const OPENCHAMBER_PROJECT_ACTION_COMMAND_MAX_LENGTH = 4000;
const OPENCHAMBER_PROJECT_ACTION_OPEN_URL_MAX_LENGTH = 2000;
const OPENCHAMBER_PROJECT_ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
const OPENCHAMBER_PROJECT_PLAN_TITLE_MAX_LENGTH = 160;
const OPENCHAMBER_ACTION_PLATFORM_SET = new Set<OpenChamberProjectActionPlatform>(['macos', 'linux', 'windows']);
@@ -271,93 +240,6 @@ const trimToMaxLength = (value: string, maxLength: number): string => {
return value.slice(0, maxLength);
};
const sanitizeProjectNotes = (value: unknown): string => {
if (typeof value !== 'string') {
return '';
}
return trimToMaxLength(value, OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH);
};
const sanitizeProjectTodoItems = (value: unknown): OpenChamberProjectTodoItem[] => {
if (!Array.isArray(value)) {
return [];
}
const sanitized: OpenChamberProjectTodoItem[] = [];
for (const entry of value) {
if (!entry || typeof entry !== 'object') {
continue;
}
const record = entry as {
id?: unknown;
text?: unknown;
completed?: unknown;
createdAt?: unknown;
};
const id = typeof record.id === 'string' ? record.id.trim() : '';
const textRaw = typeof record.text === 'string' ? record.text : '';
const text = trimToMaxLength(textRaw.trim(), OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH);
if (!id || !text) {
continue;
}
const completed = Boolean(record.completed);
const createdAt =
typeof record.createdAt === 'number' && Number.isFinite(record.createdAt) && record.createdAt >= 0
? record.createdAt
: Date.now();
sanitized.push({
id,
text,
completed,
createdAt,
});
}
return sanitized;
};
const sanitizeProjectPlanFileLinks = (value: unknown): OpenChamberProjectPlanFileLink[] => {
if (!Array.isArray(value)) {
return [];
}
const sanitized: OpenChamberProjectPlanFileLink[] = [];
const seenIds = new Set<string>();
for (const entry of value) {
if (!entry || typeof entry !== 'object') {
continue;
}
const record = entry as {
id?: unknown;
path?: unknown;
createdAt?: unknown;
};
const id = typeof record.id === 'string' ? record.id.trim() : '';
const path = typeof record.path === 'string' ? record.path.trim() : '';
const createdAt =
typeof record.createdAt === 'number' && Number.isFinite(record.createdAt) && record.createdAt >= 0
? record.createdAt
: Date.now();
if (!id || !path || seenIds.has(id)) {
continue;
}
seenIds.add(id);
sanitized.push({ id, path, createdAt });
}
return sanitized.sort((a, b) => b.createdAt - a.createdAt);
};
const sanitizeProjectActionPlatforms = (value: unknown): OpenChamberProjectActionPlatform[] => {
if (!Array.isArray(value)) {
return [];
@@ -457,97 +339,6 @@ const sanitizeProjectActionsState = (value: {
};
};
const sanitizeProjectNotesAndTodos = (value: {
notes?: unknown;
todos?: unknown;
} | null | undefined): OpenChamberProjectNotesTodos => {
return {
notes: sanitizeProjectNotes(value?.notes),
todos: sanitizeProjectTodoItems(value?.todos),
};
};
const sanitizeProjectContextData = (value: {
notes?: unknown;
todos?: unknown;
plans?: unknown;
} | null | undefined): OpenChamberProjectContextData => {
const notesAndTodos = sanitizeProjectNotesAndTodos(value);
return {
...notesAndTodos,
plans: sanitizeProjectPlanFileLinks(value?.plans),
};
};
const slugifyPlanTitle = (value: string): string => {
const normalized = value
.trim()
.toLowerCase()
.replace(/[`*_#>[\](){}.!?,:;"']/g, '')
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || 'plan';
};
const sanitizePlanTitle = (value: string): string => {
return trimToMaxLength(value.trim(), OPENCHAMBER_PROJECT_PLAN_TITLE_MAX_LENGTH);
};
const createProjectPlanId = (): string => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `plan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
};
const getProjectStorageDirectory = async (project: ProjectRef): Promise<string | null> => {
const base = await getUserProjectsDirectory();
const safeId = resolveConfigProjectId(project);
if (!base || !safeId) {
return null;
}
return joinPath(base, safeId);
};
const getProjectPlansDirectory = async (project: ProjectRef): Promise<string | null> => {
const projectDirectory = await getProjectStorageDirectory(project);
if (!projectDirectory) {
return null;
}
return joinPath(projectDirectory, 'plans');
};
const formatProjectPlanMarkdown = (title: string, body: string): string => {
const normalizedTitle = sanitizePlanTitle(title) || 'Plan';
const normalizedBody = body.trim();
return normalizedBody
? `# ${normalizedTitle}\n\n${normalizedBody}`
: `# ${normalizedTitle}\n`;
};
export const parseProjectPlanMarkdown = (raw: string): { title: string; body: string } => {
const text = typeof raw === 'string' ? raw : '';
const normalized = text.replace(/\r\n?/g, '\n');
const match = normalized.match(/^\s*#\s+(.+?)\s*(?:\n+|$)/);
if (match) {
const title = sanitizePlanTitle(match[1]);
const body = normalized.slice(match[0].length).replace(/^\n+/, '');
return {
title: title || 'Plan',
body,
};
}
const firstNonEmptyLine = normalized.split('\n').map((line) => line.trim()).find(Boolean) || 'Plan';
return {
title: sanitizePlanTitle(firstNonEmptyLine.replace(/^#+\s*/, '')) || 'Plan',
body: normalized.trim(),
};
};
/**
* Read the config for a project.
* Returns null if file doesn't exist or is invalid.
@@ -721,171 +512,6 @@ export async function saveProjectDraftStarters(project: ProjectRef, starters: Dr
return updateOpenChamberConfig(project, { draftStarters: sanitizeStarterRefs(starters) });
}
export async function getProjectNotesAndTodos(project: ProjectRef): Promise<OpenChamberProjectNotesTodos> {
const config = await readOpenChamberConfig(project);
return sanitizeProjectNotesAndTodos({
notes: config?.projectNotes,
todos: config?.projectTodos,
});
}
export async function saveProjectNotesAndTodos(
project: ProjectRef,
value: OpenChamberProjectNotesTodos
): Promise<boolean> {
const sanitized = sanitizeProjectNotesAndTodos({
notes: value.notes,
todos: value.todos,
});
return updateOpenChamberConfig(project, {
projectNotes: sanitized.notes,
projectTodos: sanitized.todos,
});
}
export async function getProjectContextData(project: ProjectRef): Promise<OpenChamberProjectContextData> {
const config = await readOpenChamberConfig(project);
return sanitizeProjectContextData({
notes: config?.projectNotes,
todos: config?.projectTodos,
plans: config?.projectPlanFiles,
});
}
async function getProjectPlanFiles(project: ProjectRef): Promise<OpenChamberProjectPlanFileLink[]> {
const config = await readOpenChamberConfig(project);
return sanitizeProjectPlanFileLinks(config?.projectPlanFiles);
}
async function saveProjectPlanFiles(
project: ProjectRef,
value: OpenChamberProjectPlanFileLink[]
): Promise<boolean> {
const sanitized = sanitizeProjectPlanFileLinks(value);
return updateOpenChamberConfig(project, {
projectPlanFiles: sanitized,
});
}
export async function readProjectPlanFile(path: string): Promise<OpenChamberProjectPlanFile | null> {
const trimmedPath = typeof path === 'string' ? path.trim() : '';
if (!trimmedPath) {
return null;
}
const raw = await readTextFile(trimmedPath);
if (raw === null) {
return null;
}
const parsed = parseProjectPlanMarkdown(raw);
return {
title: parsed.title,
body: parsed.body,
raw,
path: trimmedPath,
};
}
const deleteFile = async (path: string): Promise<boolean> => {
const runtimeFiles = getRuntimeFilesAPI();
if (runtimeFiles?.delete) {
try {
const result = await runtimeFiles.delete(path);
if (result?.success !== false) {
return true;
}
} catch {
// fall through
}
}
const res = await postJson<{ success?: boolean }>(`${getBaseUrl()}/fs/delete`, { path });
return Boolean(res.ok);
};
export async function deleteProjectPlanFile(
project: ProjectRef,
planId: string
): Promise<boolean> {
const trimmedId = typeof planId === 'string' ? planId.trim() : '';
if (!trimmedId) {
return false;
}
const existing = await getProjectPlanFiles(project);
const target = existing.find((entry) => entry.id === trimmedId);
if (!target) {
return false;
}
const next = existing.filter((entry) => entry.id !== trimmedId);
const saved = await saveProjectPlanFiles(project, next);
if (!saved) {
return false;
}
// Best-effort: remove underlying markdown file, ignore failure.
await deleteFile(target.path).catch(() => false);
return true;
}
export async function importProjectPlanFileFromContent(
project: ProjectRef,
content: string,
fallbackTitle?: string
): Promise<OpenChamberProjectPlanFileLink | null> {
const raw = typeof content === 'string' ? content : '';
if (!raw.trim()) {
return null;
}
const parsed = parseProjectPlanMarkdown(raw);
const title = parsed.title || sanitizePlanTitle(fallbackTitle ?? '') || 'Plan';
return createProjectPlanFile(project, { title, body: parsed.body });
}
export async function createProjectPlanFile(
project: ProjectRef,
value: { title: string; body: string }
): Promise<OpenChamberProjectPlanFileLink | null> {
const plansDirectory = await getProjectPlansDirectory(project);
if (!plansDirectory) {
return null;
}
const title = sanitizePlanTitle(value.title) || 'Plan';
const createdAt = Date.now();
const id = createProjectPlanId();
const filePath = joinPath(plansDirectory, `${createdAt}-${slugifyPlanTitle(title)}.md`);
const projectDirectory = await getProjectStorageDirectory(project);
if (!projectDirectory) {
return null;
}
const createdProjectDir = await mkdirp(projectDirectory);
const createdPlansDir = createdProjectDir ? await mkdirp(plansDirectory) : false;
if (!createdProjectDir || !createdPlansDir) {
return null;
}
const wrote = await writeTextFile(filePath, formatProjectPlanMarkdown(title, value.body));
if (!wrote) {
return null;
}
const existing = await getProjectPlanFiles(project);
const nextEntry = { id, path: filePath, createdAt };
const saved = await saveProjectPlanFiles(project, [nextEntry, ...existing]);
if (!saved) {
return null;
}
return nextEntry;
}
export async function getProjectActionsState(project: ProjectRef): Promise<OpenChamberProjectActionsState> {
const config = await readOpenChamberConfig(project);
return sanitizeProjectActionsState({
+32 -1
View File
@@ -31,7 +31,22 @@ type BrowserControlRequestEvent = {
parameters: Record<string, unknown>;
};
type OpenChamberEvent = ScheduledTaskRanEvent | SessionCreatedEvent | BrowserControlRequestEvent;
/**
* The agent changed what it remembers. Carries only which store moved, not the
* entries: listeners re-read from the server, so the event cannot go stale
* between being sent and being handled.
*/
type AgentMemoryChangedEvent = {
type: 'agent-memory-changed';
scope: 'global' | 'project';
projectId?: string;
};
type OpenChamberEvent =
| ScheduledTaskRanEvent
| SessionCreatedEvent
| BrowserControlRequestEvent
| AgentMemoryChangedEvent;
type Listener = (event: OpenChamberEvent) => void;
let eventSource: EventSource | null = null;
@@ -118,6 +133,22 @@ const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) =
return;
}
if (envelope.type === 'openchamber:agent-memory-changed') {
const properties = getEventProperties(envelope.properties);
const scope = properties?.scope === 'project' ? 'project' : 'global';
const nextEvent: AgentMemoryChangedEvent = {
type: 'agent-memory-changed',
scope,
...(typeof properties?.projectId === 'string' && properties.projectId.length > 0
? { projectId: properties.projectId }
: {}),
};
for (const listener of listeners) {
listener(nextEvent);
}
return;
}
if (envelope.type === 'openchamber:session-created') {
const properties = getEventProperties(envelope.properties);
const sessionId = typeof properties?.sessionId === 'string' ? properties.sessionId : '';
@@ -9,6 +9,7 @@ let configCalls = 0;
let runtimeKey = 'test-runtime';
const promptAsyncCalls: unknown[][] = [];
const promptAsyncResults: Array<unknown> = [];
const pathGetResults: Array<unknown> = [];
const promptAsyncMock = mock(async (...args: unknown[]) => {
promptAsyncCalls.push(args);
@@ -17,6 +18,12 @@ const promptAsyncMock = mock(async (...args: unknown[]) => {
return next ?? { response: new Response(null, { status: 200 }) };
});
const pathGetMock = mock(async () => {
const next = pathGetResults.shift();
if (next instanceof Error) throw next;
return next ?? { data: { directory: '/workspace/project' } };
});
mock.module('@opencode-ai/sdk/v2', () => ({
createOpencodeClient: mock(() => ({
config: {
@@ -30,6 +37,9 @@ mock.module('@opencode-ai/sdk/v2', () => ({
session: {
promptAsync: promptAsyncMock,
},
path: {
get: pathGetMock,
},
})),
}));
@@ -64,6 +74,17 @@ beforeEach(() => {
runtimeKey = 'test-runtime';
promptAsyncCalls.length = 0;
promptAsyncResults.length = 0;
pathGetResults.length = 0;
});
describe('opencodeClient directory availability', () => {
test('distinguishes a missing directory from an unavailable path probe', async () => {
pathGetResults.push({ error: { code: 'ENOENT', message: 'no such file or directory' } });
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('missing');
pathGetResults.push(new Error('offline'));
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('unknown');
});
});
describe('opencodeClient getConfig cache', () => {
+33 -7
View File
@@ -68,6 +68,21 @@ type SdkResult<T> = {
response?: { status?: number };
};
type DirectoryAvailability = "available" | "missing" | "unknown";
const isMissingDirectoryError = (error: unknown): boolean => {
if (error instanceof FilesystemError) {
return error.reason === "not-found" || error.reason === "not-directory";
}
if (error && typeof error === "object") {
const code = (error as { code?: unknown }).code;
if (code === "ENOENT" || code === "ENOTDIR") {
return true;
}
}
return /\bENOENT\b|\bENOTDIR\b|no such file or directory/i.test(formatSdkError(error));
};
function unwrapSdkData<T>(result: SdkResult<T>, operation: string): T {
if (result.error) {
const status = result.response?.status;
@@ -506,17 +521,28 @@ class OpencodeService {
* This is intentionally NOT the same as local filesystem access in the UI runtime.
*/
async probeDirectory(directory: string): Promise<boolean> {
return (await this.getDirectoryAvailability(directory)) === "available";
}
/**
* Distinguishes a confirmed-missing directory from an unavailable probe.
* Offline, permission, and other transport failures stay `unknown` so callers
* do not treat a temporary outage as proof the path was deleted.
*/
async getDirectoryAvailability(directory: string): Promise<DirectoryAvailability> {
const normalized = this.normalizeCandidatePath(directory);
if (!normalized) {
return false;
return "unknown";
}
try {
const response = await this.client.path.get({ directory: normalized });
const info = response.data as { directory?: unknown } | undefined;
const returned = typeof info?.directory === 'string' ? info.directory : null;
return Boolean(returned && returned.trim().length > 0);
} catch {
return false;
const response = await this.client.path.get({ directory: normalized }) as SdkResult<{ directory?: unknown }>;
if (response.error) {
return isMissingDirectoryError(response.error) ? "missing" : "unknown";
}
const returned = typeof response.data?.directory === "string" ? response.data.directory.trim() : "";
return returned ? "available" : "unknown";
} catch (error) {
return isMissingDirectoryError(error) ? "missing" : "unknown";
}
}
+41
View File
@@ -241,6 +241,47 @@ describe('updateDesktopSettings', () => {
}
});
test('sanitizes a successful fallback settings response before applying it', async () => {
const previousFetch = globalThis.fetch;
const fallbackFetch: typeof fetch = async () => new Response(JSON.stringify({ terminalShell: 'zsh' }), {
headers: { 'Content-Type': 'application/json' },
});
try {
globalThis.fetch = fallbackFetch;
useUIStore.getState().setTerminalShell('fish');
await updateDesktopSettings({ terminalShell: 'zsh' });
expect(useUIStore.getState().terminalShell).toBe('zsh');
expect(getSettingsSaveState()).toBe('idle');
} finally {
globalThis.fetch = previousFetch;
}
});
test('reports an error without applying a malformed fallback settings response', async () => {
const previousFetch = globalThis.fetch;
const fallbackFetch: typeof fetch = async () => new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
});
const states: string[] = [];
const unsubscribe = subscribeToSettingsSaveState(() => {
states.push(getSettingsSaveState());
});
try {
globalThis.fetch = fallbackFetch;
useUIStore.getState().setTerminalShell('fish');
await updateDesktopSettings({ terminalShell: 'zsh' });
expect(useUIStore.getState().terminalShell).toBe('fish');
expect(states).toEqual(['saving', 'error']);
} finally {
unsubscribe();
globalThis.fetch = previousFetch;
}
});
test('drains a pending save to the previous runtime and ignores its stale response', async () => {
switchRuntimeEndpoint({ apiBaseUrl: 'https://settings-a.example', runtimeKey: 'settings-a' });
const saveResult = deferred<SettingsPayload>();
+31 -17
View File
@@ -130,7 +130,7 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
if (Array.isArray(settings.projects) && settings.projects.length > 0) {
const collapsed = settings.projects
.filter((project) => (project as unknown as { sidebarCollapsed?: boolean }).sidebarCollapsed === true)
.filter((project) => project.sidebarCollapsed === true)
.map((project) => project.id)
.filter((id): id is string => typeof id === 'string' && id.length > 0);
if (collapsed.length > 0) {
@@ -273,13 +273,14 @@ const sanitizeSkillCatalogs = (value: unknown): DesktopSettings['skillCatalogs']
if (seen.has(id)) continue;
seen.add(id);
result.push({
const catalog: NonNullable<DesktopSettings['skillCatalogs']>[number] = {
id,
label,
source,
...(subpath ? { subpath } : {}),
...(gitIdentityId ? { gitIdentityId } : {}),
});
};
if (subpath) catalog.subpath = subpath;
if (gitIdentityId) catalog.gitIdentityId = gitIdentityId;
result.push(catalog);
}
return result;
@@ -393,7 +394,7 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin
project.icon = candidate.icon.trim();
}
if (candidate.iconImage === null) {
(project as unknown as Record<string, unknown>).iconImage = null;
project.iconImage = null;
} else if (candidate.iconImage && typeof candidate.iconImage === 'object') {
const iconImage = candidate.iconImage as Record<string, unknown>;
const mime = typeof iconImage.mime === 'string' ? iconImage.mime.trim() : '';
@@ -404,18 +405,18 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin
? iconImage.source
: null;
if (mime && updatedAt > 0 && source) {
(project as unknown as Record<string, unknown>).iconImage = { mime, updatedAt, source };
project.iconImage = { mime, updatedAt, source };
}
}
if (typeof candidate.color === 'string' && candidate.color.trim().length > 0) {
project.color = candidate.color.trim();
}
if (candidate.iconBackground === null) {
(project as unknown as Record<string, unknown>).iconBackground = null;
project.iconBackground = null;
} else {
const iconBackground = normalizeIconBackground(candidate.iconBackground);
if (iconBackground) {
(project as unknown as Record<string, unknown>).iconBackground = iconBackground;
project.iconBackground = iconBackground;
}
}
if (typeof candidate.addedAt === 'number' && Number.isFinite(candidate.addedAt) && candidate.addedAt >= 0) {
@@ -429,7 +430,7 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin
project.lastOpenedAt = candidate.lastOpenedAt;
}
if (typeof candidate.sidebarCollapsed === 'boolean') {
(project as unknown as Record<string, unknown>).sidebarCollapsed = candidate.sidebarCollapsed;
project.sidebarCollapsed = candidate.sidebarCollapsed;
}
result.push(project);
}
@@ -507,7 +508,7 @@ const sanitizeModelRefs = (value: unknown, limit: number): Array<{ providerID: s
};
const getPersistApi = (): PersistApi | undefined => {
const candidate = (useUIStore as unknown as { persist?: PersistApi }).persist;
const candidate = useUIStore.persist;
if (candidate && typeof candidate === 'object') {
return candidate;
}
@@ -554,6 +555,7 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications,
agentControlToolEnabled: defaults.agentControlToolEnabled,
agentWebToolEnabled: defaults.agentWebToolEnabled,
agentMemoryToolEnabled: defaults.agentMemoryToolEnabled,
showToolFileIcons: defaults.showToolFileIcons,
codeBlockLineWrap: defaults.codeBlockLineWrap,
showTurnChangedFiles: defaults.showTurnChangedFiles,
@@ -736,6 +738,19 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
) {
store.setAgentWebToolEnabled(settings.agentWebToolEnabled);
}
if (
typeof settings.agentMemoryToolEnabled === 'boolean'
&& settings.agentMemoryToolEnabled !== store.agentMemoryToolEnabled
) {
store.setAgentMemoryToolEnabled(settings.agentMemoryToolEnabled);
}
// Server-owned: it says whether this build has the feature at all.
if (
typeof settings.agentMemoryFeatureAvailable === 'boolean'
&& settings.agentMemoryFeatureAvailable !== store.agentMemoryFeatureAvailable
) {
store.setAgentMemoryFeatureAvailable(settings.agentMemoryFeatureAvailable);
}
if (typeof settings.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) {
store.setShowToolFileIcons(settings.showToolFileIcons);
}
@@ -1325,11 +1340,7 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
for (const [providerId, config] of Object.entries(candidate.usageModelGroups)) {
if (config && typeof config === 'object') {
const typedConfig = config as Record<string, unknown>;
const providerConfig: {
customGroups?: Array<{id: string; label: string; models: string[]; order: number}>;
modelAssignments?: Record<string, string>;
renamedGroups?: Record<string, string>;
} = {};
const providerConfig: NonNullable<DesktopSettings['usageModelGroups']>[string] = {};
// Parse customGroups
if (Array.isArray(typedConfig.customGroups)) {
@@ -1385,6 +1396,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.agentWebToolEnabled === 'boolean') {
result.agentWebToolEnabled = candidate.agentWebToolEnabled;
}
if (typeof candidate.agentMemoryToolEnabled === 'boolean') {
result.agentMemoryToolEnabled = candidate.agentMemoryToolEnabled;
}
if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
result.openCodeUpdateToastDismissedVersion = candidate.openCodeUpdateToastDismissedVersion.trim().slice(0, 128);
}
@@ -1875,7 +1889,7 @@ async function _flushSettingsUpdate(): Promise<void> {
return;
}
const updated = (await response.json().catch(() => null)) as DesktopSettings | null;
const updated = sanitizeWebSettings(await response.json().catch(() => null));
if (!isSettingsRuntimeContextCurrent(context)) return;
if (updated) {
applyDesktopUiPreferences(updated);
+339
View File
@@ -0,0 +1,339 @@
/**
* Client for the OpenChamber project context routes.
*
* Notes, todos, and plan markdown are owned by the server
* (`packages/web/server/lib/project-context`). This module only speaks HTTP:
* it resolves no storage paths and never reads plan files directly, so the
* shared UI has no knowledge of where any of it lives on disk.
*
* Every function throws on failure. An authoritative read must never resolve
* to an empty value that a caller could mistake for "the project has nothing".
*/
import { createProjectIdFromPath } from './projectId';
import { runtimeFetch } from './runtime-fetch';
export interface ProjectTodoItem {
id: string;
text: string;
completed: boolean;
createdAt: number;
}
export interface ProjectPlanLink {
id: string;
file: string;
title: string;
createdAt: number;
pinned: boolean;
}
export type ProjectNoteSource = 'manual' | 'selection' | 'agent';
export interface ProjectNote {
id: string;
body: string;
createdAt: number;
updatedAt: number;
source: ProjectNoteSource;
pinned: boolean;
/** The message this note was distilled from, when it came from a chat. */
origin?: { sessionId: string; messageId?: string };
}
interface ProjectContextData {
notes: ProjectNote[];
todos: ProjectTodoItem[];
plans: ProjectPlanLink[];
}
interface ProjectPlanContent extends ProjectPlanLink {
body: string;
raw: string;
}
export interface ProjectRef {
id: string;
path: string;
}
export const PROJECT_NOTE_BODY_MAX_LENGTH = 3000;
export const PROJECT_TODO_TEXT_MAX_LENGTH = 120;
/**
* Split a plan document into title and body, mirroring the server's own rule so
* an unsaved editor buffer and an imported file title exactly the way the
* stored file will.
*/
export const parsePlanMarkdown = (raw: string, fallback: string): { title: string; body: string } => {
const normalized = (typeof raw === 'string' ? raw : '').replace(/\r\n?/g, '\n');
const heading = normalized.match(/^\s*#\s+(.+?)\s*(?:\n+|$)/);
if (heading) {
return {
title: heading[1].trim() || fallback,
body: normalized.slice(heading[0].length).replace(/^\n+/, ''),
};
}
const firstLine = normalized.split('\n').map((line) => line.trim()).find(Boolean);
return {
title: firstLine ? firstLine.replace(/^#+\s*/, '').trim() || fallback : fallback,
body: normalized.trim(),
};
};
/**
* The storage id is derived from the project path, not from `project.id`.
* Project ids in settings have churned across versions; the path-derived id is
* what the server uses to name the config file, so both sides must agree on it.
*/
export const resolveProjectContextId = (project: ProjectRef | null | undefined): string => {
const projectPath = typeof project?.path === 'string' ? project.path.trim() : '';
if (!projectPath) {
return '';
}
return createProjectIdFromPath(projectPath);
};
const basePath = (projectId: string): string => `/api/project-context/${encodeURIComponent(projectId)}`;
const requireProjectId = (project: ProjectRef): string => {
const projectId = resolveProjectContextId(project);
if (!projectId) {
throw new Error('Project has no resolvable path');
}
return projectId;
};
const readErrorMessage = async (response: Response, fallback: string): Promise<string> => {
try {
const payload = await response.json() as { error?: unknown } | null;
if (payload && typeof payload.error === 'string' && payload.error.trim()) {
return payload.error;
}
} catch {
// Fall through to the generic message.
}
return `${fallback} (${response.status})`;
};
const parseContext = (payload: unknown): ProjectContextData => {
const record = payload as Partial<ProjectContextData> | null;
if (!record || typeof record !== 'object') {
throw new Error('Malformed project context response');
}
return {
notes: Array.isArray(record.notes) ? record.notes : [],
todos: Array.isArray(record.todos) ? record.todos : [],
plans: Array.isArray(record.plans) ? record.plans : [],
};
};
export const fetchProjectContext = async (
project: ProjectRef,
options: { signal?: AbortSignal } = {},
): Promise<ProjectContextData> => {
const response = await runtimeFetch(basePath(requireProjectId(project)), {
cache: 'no-store',
signal: options.signal,
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to load project context'));
}
return parseContext(await response.json());
};
export const saveProjectTodos = async (
project: ProjectRef,
todos: ProjectTodoItem[],
): Promise<ProjectContextData> => {
const response = await runtimeFetch(`${basePath(requireProjectId(project))}/todos`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ todos }),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to save project todos'));
}
return parseContext(await response.json());
};
export const createProjectNote = async (
project: ProjectRef,
value: { body: string; source?: ProjectNoteSource; origin?: { sessionId: string; messageId?: string } },
): Promise<{ note: ProjectNote; context: ProjectContextData }> => {
const response = await runtimeFetch(`${basePath(requireProjectId(project))}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
body: value.body,
...(value.source ? { source: value.source } : {}),
...(value.origin ? { origin: value.origin } : {}),
}),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to create note'));
}
const payload = await response.json() as { note?: ProjectNote; context?: unknown };
if (!payload?.note) {
throw new Error('Malformed note create response');
}
return { note: payload.note, context: parseContext(payload.context) };
};
/**
* Patch a note. Only the supplied fields are sent, so pinning cannot roll back
* an edit that landed between the two requests.
*
* Resolves `null` when the note is gone.
*/
export const updateProjectNote = async (
project: ProjectRef,
noteId: string,
patch: { body?: string; pinned?: boolean },
): Promise<ProjectNote | null> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/notes/${encodeURIComponent(noteId)}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
},
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to save note'));
}
const payload = await response.json() as { note?: ProjectNote };
if (!payload?.note) {
throw new Error('Malformed note save response');
}
return payload.note;
};
export const deleteProjectNote = async (
project: ProjectRef,
noteId: string,
): Promise<ProjectContextData> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/notes/${encodeURIComponent(noteId)}`,
{ method: 'DELETE' },
);
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to delete note'));
}
return parseContext(await response.json());
};
/** Resolves `null` when the plan is gone. */
export const setProjectPlanPinned = async (
project: ProjectRef,
planId: string,
pinned: boolean,
): Promise<ProjectPlanLink | null> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/plans/${encodeURIComponent(planId)}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pinned }),
},
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to update plan'));
}
const payload = await response.json() as { plan?: ProjectPlanLink };
return payload?.plan ?? null;
};
/**
* Plans are addressed by id. The caller supplies content, never a path, so a
* plan can only ever be created inside the project's own plans directory.
*/
export const createProjectPlan = async (
project: ProjectRef,
value: { title: string; body: string },
): Promise<{ plan: ProjectPlanLink; context: ProjectContextData }> => {
const response = await runtimeFetch(`${basePath(requireProjectId(project))}/plans`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: value.title, body: value.body }),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to create plan'));
}
const payload = await response.json() as { plan?: ProjectPlanLink; context?: unknown };
if (!payload?.plan) {
throw new Error('Malformed plan create response');
}
return { plan: payload.plan, context: parseContext(payload.context) };
};
/** Resolves `null` only when the plan or its markdown is genuinely gone. */
export const fetchProjectPlan = async (
project: ProjectRef,
planId: string,
options: { signal?: AbortSignal } = {},
): Promise<ProjectPlanContent | null> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/plans/${encodeURIComponent(planId)}`,
{ cache: 'no-store', signal: options.signal },
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to read plan'));
}
return await response.json() as ProjectPlanContent;
};
/**
* Overwrite a plan's markdown with the editor's exact buffer.
*
* Resolves `null` when the plan or its file is gone, so an editor open on a
* deleted plan reports that instead of silently recreating it.
*/
export const updateProjectPlan = async (
project: ProjectRef,
planId: string,
raw: string,
): Promise<{ plan: ProjectPlanLink; raw: string } | null> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/plans/${encodeURIComponent(planId)}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ raw }),
},
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to save plan'));
}
const payload = await response.json() as { plan?: ProjectPlanLink; raw?: string };
if (!payload?.plan) {
throw new Error('Malformed plan save response');
}
return { plan: payload.plan, raw: typeof payload.raw === 'string' ? payload.raw : raw };
};
export const deleteProjectPlan = async (
project: ProjectRef,
planId: string,
): Promise<ProjectContextData> => {
const response = await runtimeFetch(
`${basePath(requireProjectId(project))}/plans/${encodeURIComponent(planId)}`,
{ method: 'DELETE' },
);
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Failed to delete plan'));
}
return parseContext(await response.json());
};
@@ -113,12 +113,16 @@ export function groupModelsByFamilyWithGetter<T>(
* For Google provider with gemini/ and antigravity/ prefixes:
* - Gemini 3.x models
* - All Claude models
* For the Claude provider: every model it reports a limit for.
*/
export function getDefaultModels(
providerId: QuotaProviderId,
availableModels: string[]
): string[] {
return availableModels.filter((model) => {
// Anthropic only reports a model here when that model has its own plan
// limit, so every one it names is worth showing by default.
if (providerId === 'claude') return true;
const lower = model.toLowerCase();
// Handle gemini/ and antigravity/ prefixes
const modelName = lower.includes('/') ? lower.split('/')[1] : lower;
@@ -8,6 +8,7 @@ export interface QuotaProviderMeta {
export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
{ id: 'claude', name: 'Claude' },
{ id: 'codex', name: 'Codex' },
{ id: 'command-code', name: 'Command Code' },
{ id: 'cursor', name: 'Cursor' },
{ id: 'github-copilot', name: 'GitHub Copilot' },
{ id: 'google', name: 'Google' },
+4 -2
View File
@@ -75,13 +75,15 @@ export const resolveUsageTone = (percent: number | null): 'safe' | 'warn' | 'cri
export const formatWindowLabel = (label: string): string => {
if (label === '5h') return t('quota.window.5h');
if (label === '7d') return t('quota.window.7d');
if (label === '7d-sonnet') return t('quota.window.7dSonnet');
if (label === '7d-opus') return t('quota.window.7dOpus');
if (label === 'extra_usage') return t('quota.window.extraUsage');
if (label === 'weekly') return t('quota.window.weekly');
if (label === 'daily') return t('quota.window.daily');
if (label === 'monthly') return t('quota.window.monthly');
if (label === 'credits') return t('quota.window.credits');
if (label === 'credits_balance') return t('quota.window.creditsBalance');
if (label === 'monthly_credits') return t('quota.window.monthlyCredits');
if (label === 'purchased_credits') return t('quota.window.purchasedCredits');
if (label === 'free_credits') return t('quota.window.freeCredits');
if (label === 'billing_cycle') return t('quota.window.billingCycle');
if (label === 'plan_limit') return t('quota.window.planLimit');
if (label === 'auto') return t('quota.window.auto');
+33 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test';
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import { buildRuntimeFetchUrl, isLatin1Safe, runtimeFetch, sanitizeHeadersForBrowser } from './runtime-fetch';
import { addRuntimeProxyHeaders, buildRuntimeFetchUrl, isLatin1Safe, runtimeFetch, sanitizeHeadersForBrowser } from './runtime-fetch';
import { clearRuntimeAuthCredentialProvider, setRuntimeBearerToken } from './runtime-auth';
import { configureRuntimeUrlResolver, getRuntimeUrlResolver, setRuntimeUrlResolver } from './runtime-url';
@@ -48,7 +48,39 @@ describe('buildRuntimeFetchUrl', () => {
});
});
describe('addRuntimeProxyHeaders', () => {
test('bypasses the ngrok browser interstitial for official ngrok hosts', () => {
const headers = addRuntimeProxyHeaders('https://demo.ngrok-free.app/health', new Headers());
expect(headers.get('ngrok-skip-browser-warning')).toBe('openchamber');
});
test('does not add proxy headers to non-ngrok or lookalike hosts', () => {
expect(addRuntimeProxyHeaders('https://runtime.example/health', new Headers()).has('ngrok-skip-browser-warning')).toBe(false);
expect(addRuntimeProxyHeaders('https://ngrok-free.app.evil.example/health', new Headers()).has('ngrok-skip-browser-warning')).toBe(false);
});
});
describe('runtimeFetch transport contract', () => {
test('adds the ngrok bypass header to runtime requests', async () => {
const previous = getRuntimeUrlResolver();
let capturedHeaders = new Headers();
try {
configureRuntimeUrlResolver({ apiBaseUrl: 'https://demo.ngrok-free.app' });
globalThis.fetch = async (_input, init) => {
capturedHeaders = new Headers(init?.headers);
return new Response(null, { status: 204 });
};
await runtimeFetch('/health');
expect(capturedHeaders.get('ngrok-skip-browser-warning')).toBe('openchamber');
} finally {
setRuntimeUrlResolver(previous);
globalThis.fetch = originalFetch;
}
});
test('preserves bodies from actual SDK mutation requests on same-origin runtimes', async () => {
const previous = getRuntimeUrlResolver();
const originalWindow = globalThis.window;
+30 -6
View File
@@ -30,6 +30,20 @@ const isCurrentWindowUrl = (url: URL): boolean => {
const isAbsoluteUrl = (value: string): boolean => /^[a-z][a-z\d+.-]*:\/\//i.test(value);
const isNgrokHost = (hostname: string): boolean =>
/(^|\.)ngrok(?:-free)?\.(?:app|dev|io)$/i.test(hostname);
export const addRuntimeProxyHeaders = (url: string, headers: Headers): Headers => {
try {
if (isNgrokHost(new URL(url).hostname) && !headers.has('ngrok-skip-browser-warning')) {
headers.set('ngrok-skip-browser-warning', 'openchamber');
}
} catch {
// Relative and non-HTTP runtime paths do not need proxy-specific headers.
}
return headers;
};
const appendRuntimeQuery = (url: URL, query?: RuntimeUrlQuery): void => {
if (!query) return;
const entries = query instanceof URLSearchParams ? Array.from(query.entries()) : Object.entries(query);
@@ -266,13 +280,15 @@ export const runtimeFetch = async (input: string | URL | Request, init: RuntimeF
const resolvedInput = resolveRuntimeFetchInput(input, query);
const inputHeaders = resolvedInput instanceof Request ? resolvedInput.headers : undefined;
const headers = await mergeHeaders(inputHeaders, requestInit.headers, shouldAttachRuntimeAuth(resolvedInput));
doFetch = resolvedInput instanceof Request
? () => fetch(new Request(resolvedInput, { ...requestInit, headers }))
: () => fetch(resolvedInput, { ...requestInit, headers });
url =
const resolvedUrl =
resolvedInput instanceof Request ? resolvedInput.url
: resolvedInput instanceof URL ? resolvedInput.toString()
: String(resolvedInput);
addRuntimeProxyHeaders(resolvedUrl, headers);
doFetch = resolvedInput instanceof Request
? () => fetch(new Request(resolvedInput, { ...requestInit, headers }))
: () => fetch(resolvedInput, { ...requestInit, headers });
url = resolvedUrl;
method = String(
requestInit.method ?? (resolvedInput instanceof Request ? resolvedInput.method : 'GET'),
).toUpperCase();
@@ -313,6 +329,7 @@ export const installRuntimeFetchBridge = (): void => {
const url = new URL(input);
if (isActiveRuntimeServiceUrl(url)) {
const headers = await mergeHeaders(undefined, init?.headers);
addRuntimeProxyHeaders(url.toString(), headers);
return nativeFetch(input, { ...init, headers });
}
} catch {
@@ -321,7 +338,9 @@ export const installRuntimeFetchBridge = (): void => {
return nativeFetch(input, init);
}
const headers = await mergeHeaders(undefined, init?.headers);
return nativeFetch(buildRuntimeFetchUrl(input), { ...init, headers });
const target = buildRuntimeFetchUrl(input);
addRuntimeProxyHeaders(target, headers);
return nativeFetch(target, { ...init, headers });
}
if (input instanceof URL) {
@@ -329,12 +348,15 @@ export const installRuntimeFetchBridge = (): void => {
if (!shouldResolveFetchInput(raw)) {
if (isActiveRuntimeServiceUrl(input)) {
const headers = await mergeHeaders(undefined, init?.headers);
addRuntimeProxyHeaders(input.toString(), headers);
return nativeFetch(input, { ...init, headers });
}
return nativeFetch(input, init);
}
const headers = await mergeHeaders(undefined, init?.headers);
return nativeFetch(buildRuntimeFetchUrl(raw), { ...init, headers });
const target = buildRuntimeFetchUrl(raw);
addRuntimeProxyHeaders(target, headers);
return nativeFetch(target, { ...init, headers });
}
if (input instanceof Request) {
@@ -343,6 +365,7 @@ export const installRuntimeFetchBridge = (): void => {
const url = new URL(input.url);
if (isActiveRuntimeServiceUrl(url)) {
const headers = await mergeHeaders(input.headers, init?.headers);
addRuntimeProxyHeaders(url.toString(), headers);
return nativeFetch(new Request(input, { ...init, headers }));
}
} catch {
@@ -352,6 +375,7 @@ export const installRuntimeFetchBridge = (): void => {
}
const headers = await mergeHeaders(input.headers, init?.headers);
const target = buildRuntimeFetchUrl(input.url);
addRuntimeProxyHeaders(target, headers);
const request = target === input.url ? input : new Request(target, input);
return nativeFetch(new Request(request, { ...init, headers }));
}
+144
View File
@@ -0,0 +1,144 @@
/**
* Project knowledge a session still owes, as decided by the server.
*
* The client neither assembles this text nor tracks what it has sent. It used
* to do both, which meant a session started without a UI got nothing, and a
* conversation that was compacted kept a tab-local belief that the agent still
* had context the summary had just removed.
*
* Nothing here throws. A message must go out even when its background cannot
* be fetched: sending without the block costs the agent some context, failing
* the send costs the user their message.
*/
import { runtimeFetch } from './runtime-fetch';
import { z } from 'zod';
interface SessionKnowledge {
/** Empty when the session already carries what it needs. */
text: string;
/** Reported back once the message carrying the text has actually gone out. */
signature: string;
}
const EMPTY: SessionKnowledge = { text: '', signature: '' };
export const fetchSessionKnowledge = async (
directory: string | null,
sessionId: string | null,
): Promise<SessionKnowledge> => {
if (!directory) {
return EMPTY;
}
try {
const params = new URLSearchParams({ directory });
if (sessionId) {
params.set('sessionId', sessionId);
}
const response = await runtimeFetch(`/api/session-knowledge?${params.toString()}`, {
cache: 'no-store',
});
if (!response.ok) {
return EMPTY;
}
const payload = await response.json() as Partial<SessionKnowledge> | null;
return {
text: typeof payload?.text === 'string' ? payload.text : '',
signature: typeof payload?.signature === 'string' ? payload.signature : '',
};
} catch {
return EMPTY;
}
};
/**
* Recorded after the send resolves, never before: a failed send must carry the
* block again rather than assume the agent already saw it.
*/
export const reportSessionKnowledgeDelivered = async (
directory: string | null,
sessionId: string | null,
signature: string,
): Promise<void> => {
if (!directory || !sessionId || !signature) {
return;
}
try {
await runtimeFetch('/api/session-knowledge/delivered', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ directory, sessionId, signature }),
});
} catch {
// Only means the block may be sent once more.
}
};
export interface SessionKnowledgeSummary {
notes: Array<{ id: string; body: string }>;
plans: Array<{ id: string; title: string }>;
memory: { global: number; project: number };
}
const EMPTY_SUMMARY: SessionKnowledgeSummary = { notes: [], plans: [], memory: { global: 0, project: 0 } };
/** What the session is carrying, for display. Never throws; shows nothing instead. */
export const fetchSessionKnowledgeSummary = async (
directory: string | null,
sessionId?: string | null,
): Promise<SessionKnowledgeSummary> => {
if (!directory) {
return EMPTY_SUMMARY;
}
try {
const params = new URLSearchParams({ directory });
if (sessionId) params.set('sessionId', sessionId);
const response = await runtimeFetch(
`/api/session-knowledge/summary?${params.toString()}`,
{ cache: 'no-store' },
);
if (!response.ok) {
return EMPTY_SUMMARY;
}
const payload = await response.json() as Partial<SessionKnowledgeSummary> | null;
return {
notes: Array.isArray(payload?.notes) ? payload.notes : [],
plans: Array.isArray(payload?.plans) ? payload.plans : [],
memory: {
global: typeof payload?.memory?.global === 'number' ? payload.memory.global : 0,
project: typeof payload?.memory?.project === 'number' ? payload.memory.project : 0,
},
};
} catch {
return EMPTY_SUMMARY;
}
};
export type SessionProjectContextPins = { notes: string[]; plans: string[] };
const sessionProjectContextPinsResponseSchema = z.object({
pins: z.object({ notes: z.array(z.string()), plans: z.array(z.string()) }),
});
export const setSessionProjectContextPin = async (
directory: string,
sessionId: string,
kind: 'note' | 'plan',
id: string,
pinned: boolean,
): Promise<SessionProjectContextPins | null> => {
try {
const response = await runtimeFetch('/api/session-knowledge/pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ directory, sessionId, kind, id, pinned }),
});
if (!response.ok) return null;
return sessionProjectContextPinsResponseSchema.parse(await response.json()).pins;
} catch {
return null;
}
};
+6 -1
View File
@@ -25,7 +25,8 @@ export type SettingsPageSlug =
| 'notifications'
| 'voice'
| 'tunnel'
| 'about';
| 'about'
| 'integrations';
type SettingsPageGroup =
| 'general'
@@ -201,6 +202,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{ slug: 'voice', title: 'Voice', group: 'general', kind: 'single', keywords: ['tts', 'speech', 'voice'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'tunnel', title: 'External Tunnel', group: 'projects', kind: 'single', keywords: ['tunnel', 'external', 'cloudflare', 'qr', 'remote', 'mobile', 'share'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'about', title: 'About', group: 'general', kind: 'single', keywords: ['about', 'version', 'updates', 'release', 'changelog'], isAvailable: (ctx) => ctx.isMobile && !ctx.isVSCode },
{ slug: 'integrations', title: 'Integrations', group: 'general', kind: 'single', keywords: ['integration', 'plugin', 'provider', 'oauth', 'claude', 'cursor', 'command code', 'connect', 'discord', 'telegram', 'messenger'] },
] as const;
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
@@ -286,6 +288,9 @@ export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
case 'git':
return 'git-branch';
case 'integrations':
return 'plug';
case 'usage':
return 'bar-chart-2';
case 'voice':
@@ -0,0 +1,41 @@
import { describe, expect, test } from 'bun:test';
import type { I18nKey } from '@/lib/i18n/store';
import { buildSettingsSearchResults } from './search';
const t = (key: I18nKey): string => key;
const runtimeCtx = {
isVSCode: false,
isWeb: true,
isDesktop: false,
isMobile: false,
isDesktopLocalOrigin: false,
isMac: false,
isWindows: false,
isLinux: false,
isWindowsArm64: false,
};
describe('settings search', () => {
test('finds the Claude Code third-party integration', () => {
const results = buildSettingsSearchResults({
query: 'claude',
runtimeCtx,
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'integrations.third-party.opencode-claude')).toBe(true);
});
test('finds third-party integrations by OpenChamber npm package names', () => {
const results = buildSettingsSearchResults({
query: '@openchamber/opencode-cursor',
runtimeCtx,
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'integrations.third-party.opencode-cursor-oauth')).toBe(true);
});
});
+39
View File
@@ -1,4 +1,5 @@
import type { I18nKey } from '@/lib/i18n/store';
import { useUIStore } from '@/stores/useUIStore';
import type { SettingsPageSlug, SettingsRuntimeContext } from './metadata';
import { getSettingsPageMeta } from './metadata';
@@ -489,6 +490,16 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
keywords: ['agent', 'tool', 'web', 'browser', 'page', 'preview', 'openchamber'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'sessions.agent-memory-tool',
page: 'general',
titleKey: 'settings.openchamber.tools.field.agentMemoryTool',
descriptionKey: 'settings.openchamber.tools.field.agentMemoryToolInfo',
keywords: ['agent', 'tool', 'memory', 'remember', 'recall', 'preferences', 'openchamber'],
// Unreleased: searching for a setting that is not rendered would take the
// user to an empty spot on the page.
isAvailable: (ctx) => !ctx.isVSCode && useUIStore.getState().agentMemoryFeatureAvailable,
},
{
id: 'git.github-account',
page: 'git',
@@ -933,6 +944,34 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
keywords: ['background', 'push'],
isAvailable: (ctx) => ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
},
{
id: 'integrations.third-party',
page: 'integrations',
titleKey: 'settings.integrations.thirdParty.title',
keywords: ['plugin', 'provider', 'oauth', 'install', 'update', 'remove'],
},
{
id: 'integrations.third-party.opencode-claude',
page: 'integrations',
titleKey: 'settings.integrations.thirdParty.opencodeClaude.name',
descriptionKey: 'settings.integrations.thirdParty.opencodeClaude.description',
keywords: ['claude', 'anthropic', 'claude code', 'pro', 'max', 'agent sdk', '@openchamber/opencode-claude'],
},
{
id: 'integrations.third-party.opencode-commandcode',
page: 'integrations',
titleKey: 'settings.integrations.thirdParty.opencodeCommandcode.name',
descriptionKey: 'settings.integrations.thirdParty.opencodeCommandcode.description',
keywords: ['command code', 'commandcode', 'laguna', 'poolside', 'gateway', '@openchamber/opencode-commandcode'],
},
{
id: 'integrations.third-party.opencode-cursor-oauth',
page: 'integrations',
titleKey: 'settings.integrations.thirdParty.opencodeCursorOauth.name',
descriptionKey: 'settings.integrations.thirdParty.opencodeCursorOauth.description',
keywords: ['cursor', 'oauth', 'subscription', 'openai compatible', '@openchamber/opencode-cursor'],
},
] as const;
interface BuildSettingsSearchResultsOptions {
+3 -3
View File
@@ -1,4 +1,4 @@
import { runtimeFetch } from '@/lib/runtime-fetch';
import { requestSmallModel } from '@/lib/smallModelRequest';
import { useConfigStore } from '@/stores/useConfigStore';
import { getSessionLastAssistantModel } from '@/sync/session-actions';
@@ -34,7 +34,7 @@ export async function summarizeSelectionForNotes(text: string, sessionId?: strin
const { currentProviderId, currentModelId } = useConfigStore.getState();
const preferredProviderID = sessionModel?.providerID || currentProviderId || '';
const preferredModelID = sessionModel?.modelID || currentModelId || '';
const response = await runtimeFetch('/api/small-model/generate', {
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -77,7 +77,7 @@ const GOAL_OBJECTIVE_SYSTEM_PROMPT = [
export async function distillGoalObjective(planContent: string): Promise<string | null> {
try {
const { currentProviderId, currentModelId } = useConfigStore.getState();
const response = await runtimeFetch('/api/small-model/generate', {
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+27
View File
@@ -0,0 +1,27 @@
import { toast } from 'sonner';
import { runtimeFetch } from '@/lib/runtime-fetch';
const SMALL_MODEL_TOAST_ID = 'small-model-unavailable';
const notifySmallModelUnavailable = (): void => {
toast.error('Small Model unavailable', {
id: SMALL_MODEL_TOAST_ID,
description: 'Choose another model in Settings → Sessions → Small Model and try again.',
});
};
export async function requestSmallModel(
init: RequestInit,
options: { silentStatuses?: number[] } = {},
): Promise<Response> {
try {
const response = await runtimeFetch('/api/small-model/generate', init);
if (!response.ok && !options.silentStatuses?.includes(response.status)) {
notifySmallModelUnavailable();
}
return response;
} catch (error) {
notifySmallModelUnavailable();
throw error;
}
}
+6 -3
View File
@@ -88,7 +88,7 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
descriptionKey: 'contextRail.surface.editor.description',
defaultWidthFraction: 3 / 5,
mode: 'file',
icon: 'braces',
icon: 'file-edit',
labelKey: 'contextPanel.mode.files',
availability: 'always',
},
@@ -104,9 +104,12 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
{
id: 'notes',
descriptionKey: 'contextRail.surface.notes.description',
defaultWidthFraction: 1 / 3,
// As wide as the files surface: this panel now carries a sidebar and a
// content column, and a third of the window leaves the content column too
// narrow to read a note in.
defaultWidthFraction: 3 / 5,
mode: 'notes',
icon: 'sticky-note',
icon: 'book-marked',
labelKey: 'contextRail.surface.notes',
availability: 'always',
},
+7
View File
@@ -201,6 +201,13 @@ const TOOL_METADATA: Record<string, ToolMetadata> = {
inputFields: []
},
openchamber_memory: {
displayName: 'OpenChamber Memory',
category: 'system',
outputLanguage: 'json',
inputFields: []
},
plan_enter: {
displayName: 'Plan Mode',
category: 'ai',
@@ -11,6 +11,8 @@ type WorktreeListEntry = {
const listCalls: string[] = [];
const listResolvers: Array<(value: WorktreeListEntry[]) => void> = [];
const createPayloads: unknown[] = [];
const validatePayloads: unknown[] = [];
const createdWorktree = {
head: 'abc123',
name: 'feature',
@@ -80,7 +82,14 @@ mock.module('@/lib/gitApi', () => ({
listResolvers.push(resolve);
});
},
create: mock(() => Promise.resolve(createdWorktreeResult)),
create: mock((_directory: string, payload: unknown) => {
createPayloads.push(payload);
return Promise.resolve(createdWorktreeResult);
}),
validate: mock((_directory: string, payload: unknown) => {
validatePayloads.push(payload);
return Promise.resolve({ ok: true, errors: [] });
}),
remove: mock(() => Promise.resolve({ success: true })),
},
},
@@ -91,6 +100,7 @@ const {
getLatestWorktreeMetadata,
listProjectWorktrees,
partitionWorktreesByRegisteredProject,
validateWorktreeCreate,
worktreeMapsEqual,
} = await import('./worktreeManager');
@@ -108,6 +118,8 @@ describe('worktreeManager list invalidation', () => {
beforeEach(() => {
listCalls.length = 0;
listResolvers.length = 0;
createPayloads.length = 0;
validatePayloads.length = 0;
bootstrapWatcherCalls.length = 0;
bootstrapWatcherOptions.length = 0;
createdWorktreeResult = createdWorktree;
@@ -372,3 +384,56 @@ describe('partitionWorktreesByRegisteredProject', () => {
expect(result.get('/repo')?.map((entry) => entry.path)).toEqual(['/worktrees/loose']);
});
});
describe('worktreeManager fork remote payload wiring', () => {
beforeEach(() => {
listCalls.length = 0;
listResolvers.length = 0;
createPayloads.length = 0;
validatePayloads.length = 0;
bootstrapWatcherCalls.length = 0;
bootstrapWatcherOptions.length = 0;
createdWorktreeResult = createdWorktree;
sessionState.availableWorktreesByProject = new Map();
sessionState.availableWorktrees = [];
sessionState.worktreeMetadata = new Map();
attachmentState.attachments = new Map();
});
test('validate and create forward ensureRemoteName/Url for a fork head', async () => {
const project = { id: 'project-1', path: '/repo' };
const args = {
mode: 'existing' as const,
branchName: 'feature/login',
worktreeName: 'pr-42',
existingBranch: 'remotes/pr-alice/feature/login',
setUpstream: true as const,
upstreamRemote: 'pr-alice',
upstreamBranch: 'feature/login',
ensureRemoteName: 'pr-alice',
ensureRemoteUrl: 'https://github.com/alice/openchamber.git',
};
const validation = await validateWorktreeCreate(project, args);
expect(validation.ok).toBe(true);
expect(validatePayloads).toHaveLength(1);
const validated = validatePayloads[0] as Record<string, unknown>;
expect(validated.mode).toBe('existing');
expect(validated.existingBranch).toBe('remotes/pr-alice/feature/login');
expect(validated.ensureRemoteName).toBe('pr-alice');
expect(validated.ensureRemoteUrl).toBe('https://github.com/alice/openchamber.git');
expect('pullRequest' in validated).toBe(false);
await createWorktree(project, {
...args,
returnAfterDirectoryCreated: true,
});
expect(createPayloads).toHaveLength(1);
const created = createPayloads[0] as Record<string, unknown>;
expect(created.existingBranch).toBe('remotes/pr-alice/feature/login');
expect(created.ensureRemoteName).toBe('pr-alice');
expect(created.ensureRemoteUrl).toBe('https://github.com/alice/openchamber.git');
expect(created.setUpstream).toBe(true);
expect('pullRequest' in created).toBe(false);
});
});