Merge main
This commit is contained in:
@@ -807,6 +807,8 @@ export interface VSCodeAPI {
|
||||
pickFiles?(options?: { extensions?: string[] }): Promise<unknown>;
|
||||
saveImage?(payload: unknown): Promise<unknown>;
|
||||
saveMarkdown?(payload: unknown): Promise<unknown>;
|
||||
/** Add a directory as a VS Code workspace folder; resolves with the full folder list after the add. */
|
||||
addWorkspaceFolder?(path: string): Promise<Array<{ name: string; path: string }>>;
|
||||
}
|
||||
|
||||
export interface PushSubscribePayload {
|
||||
|
||||
@@ -16,6 +16,7 @@ const upsertedSessions: unknown[] = [];
|
||||
const childStoreSessions: Session[] = [];
|
||||
const currentSessionSwitches: string[] = [];
|
||||
const metadataPatches: Array<{ sessionId: string; result: Record<string, unknown> }> = [];
|
||||
const parentSyncMessages: Message[] = [];
|
||||
|
||||
mock.module('@/lib/opencode/client', () => ({
|
||||
opencodeClient: {
|
||||
@@ -48,6 +49,7 @@ mock.module('@/stores/useGlobalSessionsStore', () => ({
|
||||
}));
|
||||
mock.module('@/sync/sync-refs', () => ({
|
||||
registerSessionDirectory: (sessionId: string, directory: string) => { registeredDirectories.push(`${sessionId}:${directory}`); },
|
||||
getSyncMessages: () => parentSyncMessages,
|
||||
getSyncChildStores: () => ({
|
||||
children: new Map([['/project', {
|
||||
getState: () => ({ session: childStoreSessions }),
|
||||
@@ -56,7 +58,7 @@ mock.module('@/sync/sync-refs', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { btwSessionTitle, startBtwSession, destroyBtwSession, promoteBtwSession, filterBtwTailMessages } =
|
||||
const { btwSessionTitle, startBtwSession, destroyBtwSession, promoteBtwSession, filterBtwTailMessages, findLastCompletedAssistantMessageID, BTW_BOUNDARY_INSTRUCTION } =
|
||||
await import('@/lib/btw');
|
||||
const { useBtwStore } = await import('@/stores/useBtwStore');
|
||||
|
||||
@@ -74,6 +76,15 @@ const record = (id: string): { info: Message; parts: Part[] } => ({
|
||||
parts: [],
|
||||
});
|
||||
|
||||
// SAFETY: `findLastCompletedAssistantMessageID` reads only `id`, `role` and
|
||||
// `time`, which are the fields spelled out here.
|
||||
const assistantMessage = (id: string, completed?: number) =>
|
||||
({ id, sessionID: 'parent-1', role: 'assistant', time: { created: 1, completed } }) as Message;
|
||||
|
||||
// SAFETY: same narrow read as `assistantMessage`.
|
||||
const userMessage = (id: string) =>
|
||||
({ id, sessionID: 'parent-1', role: 'user', time: { created: 1 } }) as Message;
|
||||
|
||||
const startInput = {
|
||||
parentSessionId: 'parent-1',
|
||||
question: 'wtf is kafka',
|
||||
@@ -90,6 +101,7 @@ beforeEach(() => {
|
||||
childStoreSessions.length = 0;
|
||||
currentSessionSwitches.length = 0;
|
||||
metadataPatches.length = 0;
|
||||
parentSyncMessages.length = 0;
|
||||
useBtwStore.setState({ byParent: {} });
|
||||
forkSessionImpl = () => Promise.reject(new Error('no forkSession stub'));
|
||||
getSessionMessagesImpl = () => Promise.resolve([record('msg-boundary')]);
|
||||
@@ -121,6 +133,17 @@ describe('filterBtwTailMessages', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findLastCompletedAssistantMessageID', () => {
|
||||
test('skips an assistant turn that is still streaming', () => {
|
||||
const messages = [assistantMessage('msg-1', 10), userMessage('msg-2'), assistantMessage('msg-3')];
|
||||
expect(findLastCompletedAssistantMessageID(messages)).toBe('msg-1');
|
||||
});
|
||||
|
||||
test('a session with no completed assistant turn has no fork point', () => {
|
||||
expect(findLastCompletedAssistantMessageID([userMessage('msg-1')])).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startBtwSession', () => {
|
||||
test('forks, marks the fork, links the parent, and routes the question to the fork', async () => {
|
||||
forkSessionImpl = (sessionId, messageId, directory) => {
|
||||
@@ -151,6 +174,45 @@ describe('startBtwSession', () => {
|
||||
expect(useBtwStore.getState().byParent).toEqual({});
|
||||
});
|
||||
|
||||
test('forks at the last completed assistant turn, not at the in-flight one', async () => {
|
||||
parentSyncMessages.push(assistantMessage('msg-1', 10), userMessage('msg-2'), assistantMessage('msg-3'));
|
||||
const forkPoints: Array<string | undefined> = [];
|
||||
forkSessionImpl = (_sessionId, messageId) => {
|
||||
forkPoints.push(messageId);
|
||||
return Promise.resolve(makeSession('fork-1', '/project'));
|
||||
};
|
||||
|
||||
await startBtwSession(startInput);
|
||||
|
||||
expect(forkPoints).toEqual(['msg-1']);
|
||||
});
|
||||
|
||||
test('the boundary falls back to the fork point when the cloned tail reads empty', async () => {
|
||||
parentSyncMessages.push(assistantMessage('msg-1', 10));
|
||||
forkSessionImpl = () => Promise.resolve(makeSession('fork-1', '/project'));
|
||||
getSessionMessagesImpl = () => Promise.resolve([]);
|
||||
|
||||
await startBtwSession(startInput);
|
||||
|
||||
// Not `null`: a null boundary would show the whole inherited transcript.
|
||||
expect(metadataPatches[0]?.result).toEqual({
|
||||
openchamber: { kind: 'btw', originalSessionID: 'parent-1', btwBoundaryMessageID: 'msg-1' },
|
||||
});
|
||||
});
|
||||
|
||||
test('the first question carries the boundary instruction as a synthetic part', async () => {
|
||||
forkSessionImpl = () => Promise.resolve(makeSession('fork-1', '/project'));
|
||||
const sentParts: unknown[] = [];
|
||||
sendMessageImpl = (...args) => {
|
||||
sentParts.push(args[6]);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
await startBtwSession(startInput);
|
||||
|
||||
expect(sentParts).toEqual([[{ text: BTW_BOUNDARY_INSTRUCTION, synthetic: true }]]);
|
||||
});
|
||||
|
||||
test('an empty parent produces a marker without a boundary', async () => {
|
||||
forkSessionImpl = () => Promise.resolve(makeSession('fork-1', '/project'));
|
||||
getSessionMessagesImpl = () => Promise.resolve([]);
|
||||
@@ -229,7 +291,9 @@ describe('promoteBtwSession', () => {
|
||||
|
||||
expect(metadataPatches).toEqual([
|
||||
{ sessionId: 'parent-1', result: {} },
|
||||
{ sessionId: 'fork-1', result: {} },
|
||||
// The fork stops being a btw session but stays marked as promoted: its
|
||||
// transcript still carries the boundary instructions.
|
||||
{ sessionId: 'fork-1', result: { openchamber: { btwPromoted: true } } },
|
||||
]);
|
||||
expect(currentSessionSwitches).toEqual(['fork-1']);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as sessionActions from '@/sync/session-actions';
|
||||
import { withBtwSessionLink, withBtwSessionMarker, withoutBtwSessionLink, withoutBtwSessionMarker } from '@/lib/sessionBtwMetadata';
|
||||
import { useBtwStore } from '@/stores/useBtwStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { getSyncChildStores, registerSessionDirectory } from '@/sync/sync-refs';
|
||||
import { getSyncChildStores, getSyncMessages, registerSessionDirectory } from '@/sync/sync-refs';
|
||||
import { Binary } from '@/sync/binary';
|
||||
|
||||
/**
|
||||
@@ -30,6 +30,76 @@ export type StartBtwInput = {
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sent as a synthetic part with every message inside a btw session.
|
||||
*
|
||||
* A btw session is a fork, so the model receives the parent's whole
|
||||
* conversation — including whatever plan was in flight when `/btw` was typed.
|
||||
* Without this the fork reads that plan as its own active task and carries on
|
||||
* with it instead of answering the side question, which is the opposite of
|
||||
* what `/btw` is for.
|
||||
*
|
||||
* The wording is deliberately position-independent: it names the history
|
||||
* inherited from the parent thread rather than "everything before this
|
||||
* boundary". The instruction rides along with each send instead of being
|
||||
* pinned once at fork time, so a positional phrasing would be re-anchored
|
||||
* every turn and would end up telling the model to disregard the btw
|
||||
* session's own earlier turns.
|
||||
*/
|
||||
export const BTW_BOUNDARY_INSTRUCTION = [
|
||||
'You are in a btw session, a side conversation forked from a main thread.',
|
||||
'The history inherited from the parent thread is reference context only. It is not your current task.',
|
||||
'Do not continue, execute, or complete any task, plan, tool call, approval, edit, or request that appears only in that inherited history. Only instructions the user sends inside this btw session are active.',
|
||||
'Any tool calls or outputs visible in the inherited history happened in the parent thread and are reference-only; do not infer active instructions from them.',
|
||||
'Sub-agents are off-limits in this btw session. Do not interact with any existing or new sub-agents, even if sub-agents were used in the inherited history.',
|
||||
'Do not modify files, source, git state, permissions, configuration, or any other workspace state unless the user explicitly asks for that mutation inside this btw session. If they do, keep it minimal, local to the request, and avoid disrupting the main thread.',
|
||||
].join('\n');
|
||||
|
||||
/**
|
||||
* Sent with every message in a session that was promoted out of `/btw`.
|
||||
*
|
||||
* `BTW_BOUNDARY_INSTRUCTION` is persisted on each message the session sent
|
||||
* while it was a side conversation, and there is no API to remove a message
|
||||
* part after the fact — so promotion cannot delete those lines, only answer
|
||||
* them. Without this, a promoted session keeps reading "no sub-agents, do not
|
||||
* touch the workspace" out of its own history, in a session that is no longer
|
||||
* a side conversation.
|
||||
*
|
||||
* It rides along with every send for the same reason the boundary does: the
|
||||
* instructions it revokes are re-read on every turn, so a one-shot notice
|
||||
* would lose its position relative to them as the conversation grows.
|
||||
*/
|
||||
export const BTW_PROMOTION_NOTICE =
|
||||
'This session started as a btw side conversation and has since been promoted to a normal session. '
|
||||
+ 'The btw constraints in the history above no longer apply: this is now the main thread, and the '
|
||||
+ 'usual tool, sub-agent and workspace permissions are in force.';
|
||||
|
||||
/** The boundary as an `additionalParts` entry for `sendMessage`. */
|
||||
const btwBoundaryParts = (): Array<{ text: string; synthetic: true }> =>
|
||||
[{ text: BTW_BOUNDARY_INSTRUCTION, synthetic: true }];
|
||||
|
||||
/**
|
||||
* The parent's last assistant turn that actually finished.
|
||||
*
|
||||
* `/btw` is typically typed *while* the main thread is working — that is the
|
||||
* moment a side question comes up. Forking at HEAD then clones a turn that is
|
||||
* still streaming: the fork inherits a truncated assistant message and the
|
||||
* user instruction that provoked it as the newest, most salient thing in its
|
||||
* context. Anchoring the fork to the last completed turn instead means the
|
||||
* inherited transcript is always a settled conversation.
|
||||
*
|
||||
* Returns `null` when the parent has no completed assistant turn yet (a brand
|
||||
* new session); the caller then keeps the previous fork-at-HEAD behavior.
|
||||
*/
|
||||
export const findLastCompletedAssistantMessageID = (messages: readonly Message[]): string | null => {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (message?.role !== 'assistant') continue;
|
||||
if (message.time.completed !== undefined) return message.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const btwSessionTitle = (question: string): string => `btw: ${question}`;
|
||||
|
||||
/**
|
||||
@@ -53,7 +123,16 @@ export async function startBtwSession(input: StartBtwInput): Promise<Session> {
|
||||
setPanelState(input.parentSessionId, { creating: true });
|
||||
try {
|
||||
await sessionActions.waitForConnectionOrThrow();
|
||||
const forked = await opencodeClient.forkSession(input.parentSessionId, undefined, input.directory);
|
||||
// Fork at the parent's last completed assistant turn rather than at HEAD,
|
||||
// so a `/btw` typed mid-turn does not inherit a half-finished one.
|
||||
const forkPointMessageID = findLastCompletedAssistantMessageID(
|
||||
getSyncMessages(input.parentSessionId, input.directory),
|
||||
);
|
||||
const forked = await opencodeClient.forkSession(
|
||||
input.parentSessionId,
|
||||
forkPointMessageID ?? undefined,
|
||||
input.directory,
|
||||
);
|
||||
|
||||
// The server may canonicalize the worktree path; the prompt must use the
|
||||
// same directory identity as the forked session.
|
||||
@@ -67,7 +146,14 @@ export async function startBtwSession(input: StartBtwInput): Promise<Session> {
|
||||
// id of the newest cloned message. Message ids are server-generated and
|
||||
// ascending, so everything the fork produces sorts after it.
|
||||
const newestCloned = await opencodeClient.getSessionMessages(forked.id, 1, sessionDirectory);
|
||||
const boundaryMessageID = newestCloned[newestCloned.length - 1]?.info.id ?? null;
|
||||
// A `null` boundary makes the panel show every inherited message, so an
|
||||
// empty read must not be taken as "the fork inherited nothing" when we
|
||||
// know it did: having picked a fork point proves the parent had turns.
|
||||
// Fall back to that id — the fork's own messages are created later and
|
||||
// still sort after it, so the tail stays complete either way.
|
||||
const boundaryMessageID = newestCloned[newestCloned.length - 1]?.info.id
|
||||
?? forkPointMessageID
|
||||
?? null;
|
||||
|
||||
// The fork inherits the parent's metadata and title wholesale: replace
|
||||
// the metadata with the btw marker, and rename it (rename is
|
||||
@@ -95,7 +181,10 @@ export async function startBtwSession(input: StartBtwInput): Promise<Session> {
|
||||
input.agent,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
// The very first question already needs the boundary: the fork is at
|
||||
// its most dangerous here, with the parent's in-flight plan as the
|
||||
// newest thing in its context.
|
||||
btwBoundaryParts(),
|
||||
input.variant,
|
||||
'normal',
|
||||
{ sessionId: forked.id, directory: sessionDirectory },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { copyMarkdownToClipboard } from './clipboard';
|
||||
import { flattenAssistantTextParts } from './messages/messageText';
|
||||
|
||||
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
||||
const originalClipboardItem = Object.getOwnPropertyDescriptor(globalThis, 'ClipboardItem');
|
||||
@@ -84,4 +87,52 @@ describe('copyMarkdownToClipboard', () => {
|
||||
expect(result).toEqual({ ok: true, method: 'clipboard' });
|
||||
expect(fallbackText).toBe('# title');
|
||||
});
|
||||
|
||||
test('assistant copy payload keeps Markdown block separation in every clipboard format', async () => {
|
||||
let writtenItem: { data: Record<string, Blob> } | undefined;
|
||||
class FakeClipboardItem {
|
||||
static supports(type: string): boolean {
|
||||
return type === 'text/markdown';
|
||||
}
|
||||
|
||||
readonly data: Record<string, Blob>;
|
||||
|
||||
constructor(data: Record<string, Blob>) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, 'ClipboardItem', { configurable: true, value: FakeClipboardItem });
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: {
|
||||
clipboard: {
|
||||
write: async (items: Array<{ data: Record<string, Blob> }>) => {
|
||||
writtenItem = items[0];
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const parts = [
|
||||
{ id: 'p0', sessionID: 's', messageID: 'm', type: 'text', text: '第一段' },
|
||||
{ id: 'p1', sessionID: 's', messageID: 'm', type: 'text', text: '第二段' },
|
||||
{ id: 'p2', sessionID: 's', messageID: 'm', type: 'text', text: '```js\nconsole.log(1)\n\n\nconsole.log(2)\n```' },
|
||||
{ id: 'p3', sessionID: 's', messageID: 'm', type: 'text', text: '第三段' },
|
||||
];
|
||||
|
||||
// Same path as ChatMessage.tsx handleCopyMessage:
|
||||
const text = flattenAssistantTextParts(parts as Parameters<typeof flattenAssistantTextParts>[0]);
|
||||
const html = marked.parse(text, { gfm: true, breaks: false }) as string;
|
||||
const result = await copyMarkdownToClipboard(text, html);
|
||||
|
||||
const expected = '第一段\n\n第二段\n\n```js\nconsole.log(1)\n\n\nconsole.log(2)\n```\n\n第三段';
|
||||
expect(result).toEqual({ ok: true, method: 'clipboard' });
|
||||
expect(await writtenItem?.data['text/plain']?.text()).toBe(expected);
|
||||
expect(await writtenItem?.data['text/markdown']?.text()).toBe(expected);
|
||||
const htmlText = await writtenItem?.data['text/html']?.text();
|
||||
expect(htmlText).toContain('<p>第一段</p>');
|
||||
expect(htmlText).toContain('<p>第二段</p>');
|
||||
expect(htmlText).not.toContain('<p>第一段\n第二段</p>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const MAX_OPEN_FILE_LINES = 5_000;
|
||||
export const MAX_OPEN_FILE_LINES = 20_000;
|
||||
|
||||
export const countLinesWithLimit = (content: string, limit: number): number => {
|
||||
if (!content) {
|
||||
|
||||
@@ -348,7 +348,7 @@ export const dict = {
|
||||
'multirun.launcher.attachments.attach': 'Anhängen',
|
||||
'multirun.launcher.attachments.tooltip': 'Denselben Dateien an alle Durchläufe senden',
|
||||
'multirun.launcher.models.label': 'Modelle',
|
||||
'multirun.launcher.models.info': 'Wählen Sie 2-{max} Modelle. Das gleiche Modell kann mehrfach hinzugefügt werden.',
|
||||
'multirun.launcher.models.info': 'Wählen Sie 2 oder mehr Modelle. Das gleiche Modell kann mehrfach hinzugefügt werden.',
|
||||
'multirun.launcher.toast.fileTooLarge': 'Datei "{fileName}" ist zu groß (max. 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': 'Fehler beim Anhängen von "{fileName}"',
|
||||
'multirun.launcher.toast.attachedSingle': '{count} Datei angehängt',
|
||||
@@ -2107,6 +2107,7 @@ export const dict = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': 'Anhänge sind zu groß zum Senden. Bitte versuche, die Anzahl oder Größe der Bilder zu reduzieren.',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': 'Fehler beim Senden der Anhänge. Versuche weniger Dateien oder kleinere Bilder.',
|
||||
'chat.chatInput.toast.messageSendFailed': 'Nachricht konnte nicht gesendet werden. Anhänge wurden wiederhergestellt.',
|
||||
'chat.chatInput.toast.noModelSelected': 'Wähle vor dem Senden einen Anbieter und ein Modell aus.',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': 'Fehler beim Anhängen des Bildes aus der Zwischenablage',
|
||||
'chat.chatInput.toast.addedFileMentions': '{count} Datei(er) hinzugefügt',
|
||||
'chat.chatInput.toast.attachFileFailed': 'Fehler beim Anhängen der Datei',
|
||||
@@ -2155,6 +2156,7 @@ export const dict = {
|
||||
'chat.toolPart.showRawJson': 'Rohe JSON anzeigen',
|
||||
'chat.toolPart.showFormattedJson': 'Formatierte JSON anzeigen',
|
||||
'chat.toolPart.showNavigableJson': 'Navigierbare JSON anzeigen',
|
||||
'chat.toolPart.openFile': 'Datei öffnen',
|
||||
'chat.toolPart.openFileAtFirstChange': 'Datei bei erster Änderung öffnen',
|
||||
'chat.toolPart.openFileDiff': 'Datei-Unterschied öffnen',
|
||||
'chat.toolPart.copyOutput': 'Ausgabe kopieren',
|
||||
@@ -2909,7 +2911,7 @@ export const dict = {
|
||||
'quota.window.premium': 'Premium-Interaktionen',
|
||||
'quota.window.chat': 'Chat-Anfragen',
|
||||
'quota.window.completions': 'Vervollständigungen',
|
||||
'quota.window.premiumInteractions': 'Premium-Interaktionen',
|
||||
'quota.window.premiumInteractions': 'KI-Guthaben',
|
||||
'terminalView.actions.attachSelection': 'Ausgewählte Ausgabe anhängen',
|
||||
'terminalView.actions.restart': 'Terminal neu starten',
|
||||
'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}',
|
||||
|
||||
@@ -383,7 +383,7 @@ export const dict = {
|
||||
'multirun.launcher.attachments.attach': 'Attach',
|
||||
'multirun.launcher.attachments.tooltip': 'Same files sent to all runs',
|
||||
'multirun.launcher.models.label': 'Models',
|
||||
'multirun.launcher.models.info': 'Select 2-{max} models. Same model can be added multiple times.',
|
||||
'multirun.launcher.models.info': 'Select 2 or more models. Same model can be added multiple times.',
|
||||
'multirun.launcher.toast.fileTooLarge': 'File "{fileName}" is too large (max 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': 'Failed to attach "{fileName}"',
|
||||
'multirun.launcher.toast.attachedSingle': 'Attached {count} file',
|
||||
@@ -1958,8 +1958,6 @@ export const dict = {
|
||||
'session.newWorktree.noMatchingBranches': 'No matching branches',
|
||||
'session.newWorktree.localBranches': 'Local branches',
|
||||
'session.newWorktree.remoteBranches': 'Remote branches',
|
||||
'session.newWorktree.otherLocalBranches': 'Other local branches',
|
||||
'session.newWorktree.otherRemoteBranches': 'Other remote branches',
|
||||
'session.newWorktree.branchName': 'Branch Name',
|
||||
'session.newWorktree.branchNamePlaceholder': 'feature/my-awesome-feature',
|
||||
'session.newWorktree.actions.change': 'Change',
|
||||
@@ -2302,6 +2300,7 @@ export const dict = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': 'Attachments are too large to send. Please try reducing the number or size of images.',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': 'Failed to send attachments. Try fewer files or smaller images.',
|
||||
'chat.chatInput.toast.messageSendFailed': 'Message failed to send. Attachments restored.',
|
||||
'chat.chatInput.toast.noModelSelected': 'Select a provider and model before sending.',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': 'Failed to attach image from clipboard',
|
||||
'chat.chatInput.toast.addedFileMentions': 'Added {count} file mention(s)',
|
||||
'chat.chatInput.toast.attachFileFailed': 'Failed to attach file',
|
||||
@@ -2351,6 +2350,7 @@ export const dict = {
|
||||
'chat.toolPart.showRawJson': 'Show raw JSON',
|
||||
'chat.toolPart.showFormattedJson': 'Show formatted JSON',
|
||||
'chat.toolPart.showNavigableJson': 'Show navigable JSON',
|
||||
'chat.toolPart.openFile': 'Open file',
|
||||
'chat.toolPart.openFileAtFirstChange': 'Open file at first change',
|
||||
'chat.toolPart.openFileDiff': 'Open file diff',
|
||||
'chat.toolPart.copyOutput': 'Copy output',
|
||||
@@ -3010,6 +3010,7 @@ export const dict = {
|
||||
'memoryDebugPanel.title': 'Debug Panel',
|
||||
'memoryDebugPanel.tabs.memory': 'Memory',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Requests',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'Sessions in Memory',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI Streaming Metrics',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code Bridge Metrics',
|
||||
@@ -3047,6 +3048,16 @@ export const dict = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Streaming debug JSON copied',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Failed to copy JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'Copy exports both UI and VS Code streaming metrics as JSON',
|
||||
'memoryDebugPanel.requests.inFlight': 'In flight',
|
||||
'memoryDebugPanel.requests.peak': 'Peak',
|
||||
'memoryDebugPanel.requests.duration': 'Duration',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Total Requests',
|
||||
'memoryDebugPanel.requests.tracking': 'Tracking',
|
||||
'memoryDebugPanel.requests.now': 'now',
|
||||
'memoryDebugPanel.requests.noSamples': 'No requests tracked yet. Keep this panel open to record fetch activity.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Fetch requests in flight over time, peak {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': 'last {seconds}s',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'In-flight request age percentiles (p50, p90, p99, max) over time',
|
||||
'memoryDebugPanel.common.idle': 'idle',
|
||||
'memoryDebugPanel.common.live': 'live',
|
||||
'memoryDebugPanel.common.notAvailable': 'n/a',
|
||||
@@ -3110,7 +3121,7 @@ export const dict = {
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
'quota.window.premiumInteractions': 'AI Credits',
|
||||
'chat.workStatus.ariaLabel': 'Work status',
|
||||
'chat.workStatus.context.label': 'Context',
|
||||
'chat.workStatus.cost.breakdown': 'Session {session} · Subagents {subagents}',
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"multirun.launcher.attachments.attach": "Adjuntar",
|
||||
"multirun.launcher.attachments.tooltip": "Archivos idénticos enviados a todas las ejecuciones",
|
||||
"multirun.launcher.models.label": "Modelos",
|
||||
"multirun.launcher.models.info": "Selecciona 2-{max} modelos. El mismo modelo puede añadirse varias veces.",
|
||||
"multirun.launcher.models.info": "Selecciona 2 o más modelos. El mismo modelo puede añadirse varias veces.",
|
||||
"multirun.launcher.toast.fileTooLarge": "El archivo \"{fileName}\" es demasiado grande (máximo 10MB)",
|
||||
"multirun.launcher.toast.attachFailed": "No se pudo adjuntar \"{fileName}\"",
|
||||
"multirun.launcher.toast.attachedSingle": "Archivo adjuntado ({count})",
|
||||
@@ -1936,8 +1936,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"session.newWorktree.noMatchingBranches": "No hay ramas coincidentes",
|
||||
"session.newWorktree.localBranches": "Ramas locales",
|
||||
"session.newWorktree.remoteBranches": "Ramas remotas",
|
||||
"session.newWorktree.otherLocalBranches": "Otras ramas locales",
|
||||
"session.newWorktree.otherRemoteBranches": "Otras ramas remotas",
|
||||
"session.newWorktree.branchName": "Nombre de la rama",
|
||||
"session.newWorktree.branchNamePlaceholder": "feature/my-awesome-feature",
|
||||
"session.newWorktree.actions.change": "Cambiar",
|
||||
@@ -2268,6 +2266,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.attachmentsTooLarge": "Los adjuntos son demasiado grandes para enviar. Intenta reducir la cantidad o el tamaño de las imágenes.",
|
||||
"chat.chatInput.toast.sendAttachmentsFailed": "No se pudieron enviar los adjuntos. Intenta con menos archivos o imágenes más pequeñas.",
|
||||
"chat.chatInput.toast.messageSendFailed": "El mensaje no se pudo enviar. Los adjuntos se restauraron.",
|
||||
"chat.chatInput.toast.noModelSelected": "Selecciona un proveedor y un modelo antes de enviar.",
|
||||
"chat.chatInput.toast.clipboardAttachFailed": "No se pudo adjuntar la imagen desde el portapapeles",
|
||||
"chat.chatInput.toast.addedFileMentions": "Se añadieron {count} mención(es) de archivo",
|
||||
"chat.chatInput.toast.attachFileFailed": "No se pudo adjuntar el archivo",
|
||||
@@ -2317,6 +2316,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.toolPart.showRawJson": "Mostrar JSON sin formato",
|
||||
"chat.toolPart.showFormattedJson": "Mostrar JSON formateado",
|
||||
"chat.toolPart.showNavigableJson": "Mostrar JSON navegable",
|
||||
"chat.toolPart.openFile": "Abrir archivo",
|
||||
"chat.toolPart.openFileAtFirstChange": "Abrir archivo en el primer cambio",
|
||||
"chat.toolPart.openFileDiff": "Abrir diferencias del archivo",
|
||||
"chat.toolPart.copyOutput": "Copiar salida",
|
||||
@@ -2976,6 +2976,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Panel de depuración",
|
||||
"memoryDebugPanel.tabs.memory": "Memoria",
|
||||
"memoryDebugPanel.tabs.streaming": "Transmisión",
|
||||
"memoryDebugPanel.tabs.requests": "Solicitudes",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Sesiones en memoria",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Métricas de streaming de UI",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Métricas del puente de VS Code",
|
||||
@@ -3013,6 +3014,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "JSON de depuración en streaming copiado",
|
||||
"memoryDebugPanel.streaming.copy.failed": "No se pudo copiar JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Copia exportaciones de métricas de UI como de métricas de VS Code en formato JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "En curso",
|
||||
"memoryDebugPanel.requests.peak": "Pico",
|
||||
"memoryDebugPanel.requests.duration": "Duración",
|
||||
"memoryDebugPanel.requests.totalRequests": "Solicitudes totales",
|
||||
"memoryDebugPanel.requests.tracking": "Seguimiento",
|
||||
"memoryDebugPanel.requests.now": "ahora",
|
||||
"memoryDebugPanel.requests.noSamples": "Aún no se han registrado solicitudes. Mantén este panel abierto para registrar la actividad de fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Solicitudes fetch en curso a lo largo del tiempo, pico {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "últimos {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Percentiles de antigüedad de solicitudes en curso (p50, p90, p99, máx) a lo largo del tiempo",
|
||||
"memoryDebugPanel.common.idle": "inactivo",
|
||||
"memoryDebugPanel.common.live": "en vivo",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
@@ -3111,7 +3122,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
"quota.window.premiumInteractions": "Créditos de IA",
|
||||
'chat.workStatus.ariaLabel': 'Estado del trabajo',
|
||||
'chat.workStatus.context.label': 'Contexto',
|
||||
'chat.workStatus.cost.breakdown': "Sesión {session} · Subagentes {subagents}",
|
||||
|
||||
@@ -215,7 +215,7 @@ export const dict = {
|
||||
'multirun.launcher.attachments.attach': 'Attacher',
|
||||
'multirun.launcher.attachments.tooltip': 'Mêmes fichiers envoyés à toutes les exécutions',
|
||||
'multirun.launcher.models.label': 'Modèles',
|
||||
'multirun.launcher.models.info': 'Sélectionnez les modèles 2-{max}. Le même modèle peut être ajouté plusieurs fois.',
|
||||
'multirun.launcher.models.info': 'Sélectionnez 2 modèles ou plus. Le même modèle peut être ajouté plusieurs fois.',
|
||||
'multirun.launcher.toast.fileTooLarge': 'Le fichier "{fileName}" est trop volumineux (max 10 Mo)',
|
||||
'multirun.launcher.toast.attachFailed': 'Échec de la connexion de "{fileName}"',
|
||||
'multirun.launcher.toast.attachedSingle': 'Fichier {count} joint',
|
||||
@@ -1716,8 +1716,6 @@ export const dict = {
|
||||
'session.newWorktree.noMatchingBranches': 'Aucune branche correspondante',
|
||||
'session.newWorktree.localBranches': 'Branches locales',
|
||||
'session.newWorktree.remoteBranches': 'Branches du dépôt distant',
|
||||
'session.newWorktree.otherLocalBranches': 'Autres branches locales',
|
||||
'session.newWorktree.otherRemoteBranches': 'Autres branches du remote',
|
||||
'session.newWorktree.branchName': 'Nom de la branche',
|
||||
'session.newWorktree.branchNamePlaceholder': 'fonctionnalité/ma-fonctionnalité-géniale',
|
||||
'session.newWorktree.actions.change': 'Changement',
|
||||
@@ -2015,6 +2013,7 @@ export const dict = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': 'Les pièces jointes sont trop volumineuses pour être envoyées. Veuillez essayer de réduire le nombre ou la taille des images.',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': 'Échec de l\'envoi des pièces jointes. Essayez moins de fichiers ou des images plus petites.',
|
||||
'chat.chatInput.toast.messageSendFailed': 'Le message n\'a pas pu être envoyé. Pièces jointes restaurées.',
|
||||
'chat.chatInput.toast.noModelSelected': 'Sélectionnez un fournisseur et un modèle avant d\'envoyer.',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': 'Échec de la pièce jointe de l\'image du presse-papiers',
|
||||
'chat.chatInput.toast.addedFileMentions': 'Ajout des mentions du fichier {count}',
|
||||
'chat.chatInput.toast.attachFileFailed': 'Impossible de joindre le fichier',
|
||||
@@ -2702,6 +2701,7 @@ export const dict = {
|
||||
'memoryDebugPanel.title': 'Panneau de débogage',
|
||||
'memoryDebugPanel.tabs.memory': 'Mémoire',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Requêtes',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'Sessions en mémoire',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'Métriques de streaming de l\'interface utilisateur',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'Métriques du pont VS Code',
|
||||
@@ -2739,6 +2739,16 @@ export const dict = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Débogage en streaming JSON copié',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Échec de la copie de JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'La copie exporte les métriques de streaming de l\'interface utilisateur et de VS Code en tant que JSON.',
|
||||
'memoryDebugPanel.requests.inFlight': 'En cours',
|
||||
'memoryDebugPanel.requests.peak': 'Pic',
|
||||
'memoryDebugPanel.requests.duration': 'Durée',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Requêtes totales',
|
||||
'memoryDebugPanel.requests.tracking': 'Suivi',
|
||||
'memoryDebugPanel.requests.now': 'maintenant',
|
||||
'memoryDebugPanel.requests.noSamples': 'Aucune requête enregistrée. Gardez ce panneau ouvert pour enregistrer l\'activité fetch.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Requêtes fetch en cours dans le temps, pic {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '{seconds}s dernières',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'Percentiles d\'âge des requêtes en cours (p50, p90, p99, max) dans le temps',
|
||||
'memoryDebugPanel.common.idle': 'inactif',
|
||||
'memoryDebugPanel.common.live': 'en direct',
|
||||
'memoryDebugPanel.common.notAvailable': 'n / A',
|
||||
@@ -2802,7 +2812,7 @@ export const dict = {
|
||||
'quota.window.premium': 'Interactions premium',
|
||||
'quota.window.chat': 'Requêtes de chat',
|
||||
'quota.window.completions': 'Complétions',
|
||||
'quota.window.premiumInteractions': 'Interactions premium',
|
||||
'quota.window.premiumInteractions': 'Crédits IA',
|
||||
'layout.mainTab.diagram': 'Diagramme',
|
||||
'mobile.nav.aria': 'Navigation mobile',
|
||||
'mobile.connect.welcome.title': 'Se connecter à OpenChamber',
|
||||
@@ -3092,6 +3102,7 @@ export const dict = {
|
||||
'chat.toolPart.showRawJson': 'Afficher le JSON brut',
|
||||
'chat.toolPart.showFormattedJson': 'Afficher le JSON formaté',
|
||||
'chat.toolPart.showNavigableJson': 'Afficher le JSON navigable',
|
||||
'chat.toolPart.openFile': 'Ouvrir le fichier',
|
||||
'chat.toolPart.openFileAtFirstChange': 'Ouvrir le fichier à la première modification',
|
||||
'chat.toolPart.openFileDiff': 'Ouvrir les différences du fichier',
|
||||
'chat.toolPart.copyOutput': 'Copier la sortie',
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'multirun.launcher.attachments.attach': '添付',
|
||||
'multirun.launcher.attachments.tooltip': '同じファイルをすべての実行に送信',
|
||||
'multirun.launcher.models.label': 'モデル',
|
||||
'multirun.launcher.models.info': '2~{max}モデルを選択。同じモデルを複数回追加できます。',
|
||||
'multirun.launcher.models.info': '2つ以上のモデルを選択。同じモデルを複数回追加できます。',
|
||||
'multirun.launcher.toast.fileTooLarge': 'ファイル「{fileName}」が大きすぎます(最大10MB)',
|
||||
'multirun.launcher.toast.attachFailed': '「{fileName}」の添付に失敗しました',
|
||||
'multirun.launcher.toast.attachedSingle': '{count}ファイルを添付しました',
|
||||
@@ -2298,6 +2298,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': '添付ファイルが大きすぎて送信できません。画像の数またはサイズを減らしてください。',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': '添付ファイルの送信に失敗しました。ファイルを減らすかサイズを小さくしてください。',
|
||||
'chat.chatInput.toast.messageSendFailed': 'メッセージの送信に失敗しました。添付ファイルは復元されました。',
|
||||
'chat.chatInput.toast.noModelSelected': '送信する前にプロバイダーとモデルを選択してください。',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': 'クリップボードからの画像添付に失敗しました',
|
||||
'chat.chatInput.toast.addedFileMentions': '{count}件のファイルメンションを追加しました',
|
||||
'chat.chatInput.toast.attachFileFailed': 'ファイルの添付に失敗しました',
|
||||
@@ -2350,6 +2351,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.toolPart.showRawJson': '生JSONを表示',
|
||||
'chat.toolPart.showFormattedJson': '整形JSONを表示',
|
||||
'chat.toolPart.showNavigableJson': 'ナビゲーション可能なJSONを表示',
|
||||
'chat.toolPart.openFile': 'ファイルを開く',
|
||||
'chat.toolPart.openFileAtFirstChange': '最初の変更箇所でファイルを開く',
|
||||
'chat.toolPart.openFileDiff': 'ファイル差分を開く',
|
||||
'chat.toolPart.copyOutput': '出力をコピー',
|
||||
@@ -3006,6 +3008,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': 'デバッグパネル',
|
||||
'memoryDebugPanel.tabs.memory': 'メモリ',
|
||||
'memoryDebugPanel.tabs.streaming': 'ストリーミング',
|
||||
'memoryDebugPanel.tabs.requests': 'リクエスト',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'メモリ内のセッション',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UIストリーミングメトリクス',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Codeブリッジメトリクス',
|
||||
@@ -3043,6 +3046,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'ストリーミングデバッグJSONをコピーしました',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'JSONのコピーに失敗しました',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'UIとVS Codeの両方のストリーミングメトリクスをJSONとしてエクスポートします',
|
||||
'memoryDebugPanel.requests.inFlight': '実行中',
|
||||
'memoryDebugPanel.requests.peak': 'ピーク',
|
||||
'memoryDebugPanel.requests.duration': '期間',
|
||||
'memoryDebugPanel.requests.totalRequests': '合計リクエスト',
|
||||
'memoryDebugPanel.requests.tracking': 'トラッキング',
|
||||
'memoryDebugPanel.requests.now': '現在',
|
||||
'memoryDebugPanel.requests.noSamples': 'まだリクエストが記録されていません。fetchアクティビティを記録するには、このパネルを開いたままにしてください。',
|
||||
'memoryDebugPanel.requests.chartLabel': '経時的な実行中fetchリクエスト、ピーク {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '過去 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '経時的な実行中リクエストの経過時間パーセンタイル(p50、p90、p99、最大)',
|
||||
'memoryDebugPanel.common.idle': '待機中',
|
||||
'memoryDebugPanel.common.live': 'ライブ',
|
||||
'memoryDebugPanel.common.notAvailable': 'N/A',
|
||||
@@ -3109,7 +3122,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'onboarding.localSetup.actions.checkAndContinue': 'インストール完了、確認して続行',
|
||||
'onboarding.localSetup.status.autoContinue': '検出され次第自動的に続行します。',
|
||||
'updateDialog.changelog.title': '新機能',
|
||||
'quota.window.premiumInteractions': 'プレミアムインタラクション',
|
||||
'quota.window.premiumInteractions': 'AIクレジット',
|
||||
|
||||
'chat.workStatus.ariaLabel': '作業状況',
|
||||
'chat.workStatus.context.label': 'コンテキスト',
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'multirun.launcher.attachments.attach': '첨부',
|
||||
'multirun.launcher.attachments.tooltip': '같은 파일을 모든 실행에 보냅니다',
|
||||
'multirun.launcher.models.label': '모델',
|
||||
'multirun.launcher.models.info': '모델을 2~{max}개 선택하세요. 같은 모델을 여러 번 추가할 수 있습니다.',
|
||||
'multirun.launcher.models.info': '모델을 2개 이상 선택하세요. 같은 모델을 여러 번 추가할 수 있습니다.',
|
||||
'multirun.launcher.toast.fileTooLarge': '파일 "{fileName}"이 너무 큽니다(최대 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': '"{fileName}" 첨부 실패',
|
||||
'multirun.launcher.toast.attachedSingle': '파일 {count}개 첨부됨',
|
||||
@@ -1960,8 +1960,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'session.newWorktree.noMatchingBranches': '일치하는 브랜치가 없습니다',
|
||||
'session.newWorktree.localBranches': '로컬 브랜치',
|
||||
'session.newWorktree.remoteBranches': '리모트 브랜치',
|
||||
'session.newWorktree.otherLocalBranches': '기타 로컬 브랜치',
|
||||
'session.newWorktree.otherRemoteBranches': '기타 리모트 브랜치',
|
||||
'session.newWorktree.branchName': '브랜치 이름',
|
||||
'session.newWorktree.branchNamePlaceholder': 'feature/my-awesome-feature',
|
||||
'session.newWorktree.actions.change': '변경',
|
||||
@@ -2302,6 +2300,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': '첨부 파일이 너무 커서 보낼 수 없습니다. 이미지 수나 크기를 줄여 보세요.',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': '첨부 파일 전송 실패. 파일 수나 이미지 크기를 줄여 보세요.',
|
||||
'chat.chatInput.toast.messageSendFailed': '메시지 전송에 실패했습니다. 첨부 파일을 복원했습니다.',
|
||||
'chat.chatInput.toast.noModelSelected': '전송하기 전에 제공업체와 모델을 선택하세요.',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': '클립보드 이미지 첨부 실패',
|
||||
'chat.chatInput.toast.addedFileMentions': '파일 멘션 {count}개 추가됨',
|
||||
'chat.chatInput.toast.attachFileFailed': '첨부 파일 실패',
|
||||
@@ -2351,6 +2350,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.toolPart.showRawJson': '원시 JSON 표시',
|
||||
'chat.toolPart.showFormattedJson': '형식화된 JSON 표시',
|
||||
'chat.toolPart.showNavigableJson': '탐색 가능한 JSON 표시',
|
||||
'chat.toolPart.openFile': '파일 열기',
|
||||
'chat.toolPart.openFileAtFirstChange': '첫 번째 변경 위치에서 파일 열기',
|
||||
'chat.toolPart.openFileDiff': '파일 diff 열기',
|
||||
'chat.toolPart.copyOutput': '출력 복사',
|
||||
@@ -3010,6 +3010,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '디버그 패널',
|
||||
'memoryDebugPanel.tabs.memory': '메모리',
|
||||
'memoryDebugPanel.tabs.streaming': '스트리밍',
|
||||
'memoryDebugPanel.tabs.requests': '요청',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '메모리 내 세션',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 스트리밍 지표',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 브리지 지표',
|
||||
@@ -3047,6 +3048,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '스트리밍 디버그 JSON 복사 완료',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'JSON 복사 실패',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'UI와 VS Code 스트리밍 메트릭을 JSON으로 복사합니다',
|
||||
'memoryDebugPanel.requests.inFlight': '진행 중',
|
||||
'memoryDebugPanel.requests.peak': '최대',
|
||||
'memoryDebugPanel.requests.duration': '지속 시간',
|
||||
'memoryDebugPanel.requests.totalRequests': '전체 요청',
|
||||
'memoryDebugPanel.requests.tracking': '추적 중',
|
||||
'memoryDebugPanel.requests.now': '현재',
|
||||
'memoryDebugPanel.requests.noSamples': '아직 기록된 요청이 없습니다. fetch 활동을 기록하려면 이 패널을 열어 두세요.',
|
||||
'memoryDebugPanel.requests.chartLabel': '시간에 따른 진행 중인 fetch 요청, 최대 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '최근 {seconds}초',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '진행 중 요청 수명 백분위수(p50, p90, p99, max)의 시간별 변화',
|
||||
'memoryDebugPanel.common.idle': '유휴',
|
||||
'memoryDebugPanel.common.live': '실시간',
|
||||
'memoryDebugPanel.common.notAvailable': 'n/a',
|
||||
@@ -3110,7 +3121,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
'quota.window.premiumInteractions': 'AI 크레딧',
|
||||
'chat.workStatus.ariaLabel': '작업 상태',
|
||||
'chat.workStatus.context.label': '컨텍스트',
|
||||
'chat.workStatus.cost.breakdown': '세션 {session} · 서브 에이전트 {subagents}',
|
||||
|
||||
@@ -522,7 +522,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'multirun.launcher.attachments.attach': 'Dołącz',
|
||||
'multirun.launcher.attachments.tooltip': 'Te same pliki wysłane do wszystkich uruchomień',
|
||||
'multirun.launcher.models.label': 'Modele',
|
||||
'multirun.launcher.models.info': 'Wybierz od 2 do {max} modeli. Ten sam model może być dodany wielokrotnie.',
|
||||
'multirun.launcher.models.info': 'Wybierz 2 lub więcej modeli. Ten sam model może być dodany wielokrotnie.',
|
||||
'multirun.launcher.toast.fileTooLarge': 'Plik "{fileName}" jest zbyt duży (max 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': 'Nie udało się dołączyć "{fileName}"',
|
||||
'multirun.launcher.toast.attachedSingle': 'Dołączono {count} plik',
|
||||
@@ -1278,6 +1278,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.clipboardAttachFailed': 'Nie udało się dołączyć obrazu ze schowka',
|
||||
'chat.chatInput.toast.compactFailed': 'Nie udało się skompaktować sesji',
|
||||
'chat.chatInput.toast.messageSendFailed': 'Nie udało się wysłać wiadomości. Załączniki zostały przywrócone.',
|
||||
'chat.chatInput.toast.noModelSelected': 'Wybierz dostawcę i model przed wysłaniem.',
|
||||
'chat.chatInput.toast.openSessionFirst': 'Najpierw otwórz sesję',
|
||||
'chat.chatInput.toast.reviewFailed': 'Nie udało się przejrzeć zmian',
|
||||
'chat.chatInput.toast.planFeatureFailed': 'Nie udało się rozpocząć planowania funkcji',
|
||||
@@ -1432,6 +1433,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.toolPart.showRawJson': 'Pokaż surowy JSON',
|
||||
'chat.toolPart.showFormattedJson': 'Pokaż sformatowany JSON',
|
||||
'chat.toolPart.showNavigableJson': 'Pokaż nawigowalny JSON',
|
||||
'chat.toolPart.openFile': 'Otwórz plik',
|
||||
'chat.toolPart.openFileAtFirstChange': 'Otwórz plik przy pierwszej zmianie',
|
||||
'chat.toolPart.openFileDiff': 'Otwórz różnice pliku',
|
||||
'chat.toolPart.copyOutput': 'Kopiuj wyjście',
|
||||
@@ -2568,8 +2570,19 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Skopiowano JSON debugowania streamingu',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Nie udało się skopiować JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'Kopiowanie eksportuje metryki streamingu zarówno UI, jak i VS Code w formacie JSON',
|
||||
'memoryDebugPanel.requests.inFlight': 'W trakcie',
|
||||
'memoryDebugPanel.requests.peak': 'Szczyt',
|
||||
'memoryDebugPanel.requests.duration': 'Czas trwania',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Łączne żądania',
|
||||
'memoryDebugPanel.requests.tracking': 'Śledzenie',
|
||||
'memoryDebugPanel.requests.now': 'teraz',
|
||||
'memoryDebugPanel.requests.noSamples': 'Brak żądań. Utrzymuj ten panel otwarty, aby rejestrować aktywność fetch.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Żądania fetch w trakcie w czasie, szczyt {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': 'ostatnie {seconds}s',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'Percentyle wieku żądań w trakcie (p50, p90, p99, max) w czasie',
|
||||
'memoryDebugPanel.tabs.memory': 'Pamięć',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Żądania',
|
||||
'memoryDebugPanel.title': 'Panel debugowania',
|
||||
'memoryDebugPanel.tooltip.logCurrentState': 'Zaloguj bieżący stan pamięci do konsoli przeglądarki',
|
||||
'openChamberLogo.aria.logo': 'Logo OpenChamber',
|
||||
@@ -2821,8 +2834,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'session.newWorktree.newSessionTitle': 'Nowa sesja',
|
||||
'session.newWorktree.noBranchesFound': 'Nie znaleziono gałęzi',
|
||||
'session.newWorktree.noMatchingBranches': 'Brak pasujących gałęzi',
|
||||
'session.newWorktree.otherLocalBranches': 'Pozostałe lokalne gałęzie',
|
||||
'session.newWorktree.otherRemoteBranches': 'Pozostałe zdalne gałęzie',
|
||||
'session.newWorktree.prNumber': 'PR #{number}',
|
||||
'session.newWorktree.remoteBranches': 'Zdalne gałęzie',
|
||||
'session.newWorktree.resetToMatchBranchName': 'Zresetuj do nazwy zgodnej z gałęzią',
|
||||
@@ -3127,7 +3138,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
'quota.window.premiumInteractions': 'Kredyty AI',
|
||||
'chat.workStatus.ariaLabel': 'Stan pracy',
|
||||
'chat.workStatus.context.label': 'Kontekst',
|
||||
'chat.workStatus.cost.breakdown': 'Sesja {session} · Podagenci {subagents}',
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"multirun.launcher.attachments.attach": "Anexar",
|
||||
"multirun.launcher.attachments.tooltip": "Arquivos idênticos enviados a todas as execuções",
|
||||
"multirun.launcher.models.label": "Modelos",
|
||||
"multirun.launcher.models.info": "Selecione 2-{max} modelos. O mesmo modelo pode ser adicionado várias vezes.",
|
||||
"multirun.launcher.models.info": "Selecione 2 ou mais modelos. O mesmo modelo pode ser adicionado várias vezes.",
|
||||
"multirun.launcher.toast.fileTooLarge": "O arquivo \"{fileName}\" é grande demais (máximo 10MB)",
|
||||
"multirun.launcher.toast.attachFailed": "Não foi possível anexar \"{fileName}\"",
|
||||
"multirun.launcher.toast.attachedSingle": "Arquivo anexado ({count})",
|
||||
@@ -1936,8 +1936,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"session.newWorktree.noMatchingBranches": "Não há branches coincidentes",
|
||||
"session.newWorktree.localBranches": "Branches locais",
|
||||
"session.newWorktree.remoteBranches": "Branches remotas",
|
||||
"session.newWorktree.otherLocalBranches": "Outras branches locais",
|
||||
"session.newWorktree.otherRemoteBranches": "Outras branches remotas",
|
||||
"session.newWorktree.branchName": "Nome da branch",
|
||||
"session.newWorktree.branchNamePlaceholder": "feature/my-awesome-feature",
|
||||
"session.newWorktree.actions.change": "Alterar",
|
||||
@@ -2268,6 +2266,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.attachmentsTooLarge": "Os anexos são grandes demais para enviar. Tente reduzir a quantidade ou o tamanho das imagens.",
|
||||
"chat.chatInput.toast.sendAttachmentsFailed": "Não foi possível enviar os anexos. Tente com menos arquivos ou imagens menores.",
|
||||
"chat.chatInput.toast.messageSendFailed": "A mensagem não pôde ser enviada. Os anexos foram restaurados.",
|
||||
"chat.chatInput.toast.noModelSelected": "Selecione um provedor e um modelo antes de enviar.",
|
||||
"chat.chatInput.toast.clipboardAttachFailed": "Não foi possível anexar a imagem da área de transferência",
|
||||
"chat.chatInput.toast.addedFileMentions": "Foram adicionadas {count} menção(es) de arquivo",
|
||||
"chat.chatInput.toast.attachFileFailed": "Não foi possível anexar o arquivo",
|
||||
@@ -2317,6 +2316,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.toolPart.showRawJson": "Mostrar JSON bruto",
|
||||
"chat.toolPart.showFormattedJson": "Mostrar JSON formatado",
|
||||
"chat.toolPart.showNavigableJson": "Mostrar JSON navegável",
|
||||
"chat.toolPart.openFile": "Abrir arquivo",
|
||||
"chat.toolPart.openFileAtFirstChange": "Abrir arquivo na primeira alteração",
|
||||
"chat.toolPart.openFileDiff": "Abrir diferenças do arquivo",
|
||||
"chat.toolPart.copyOutput": "Copiar saída",
|
||||
@@ -2976,6 +2976,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Painel de depuração",
|
||||
"memoryDebugPanel.tabs.memory": "Memória",
|
||||
"memoryDebugPanel.tabs.streaming": "Transmissão",
|
||||
"memoryDebugPanel.tabs.requests": "Solicitações",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Sessões em memória",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Métricas de streaming de UI",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Métricas da ponte do VS Code",
|
||||
@@ -3013,6 +3014,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "JSON de depuração em streaming copiado",
|
||||
"memoryDebugPanel.streaming.copy.failed": "Não foi possível copiar JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Copia exportações de métricas da UI e do VS Code em formato JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "Em curso",
|
||||
"memoryDebugPanel.requests.peak": "Pico",
|
||||
"memoryDebugPanel.requests.duration": "Duração",
|
||||
"memoryDebugPanel.requests.totalRequests": "Solicitações totais",
|
||||
"memoryDebugPanel.requests.tracking": "Rastreamento",
|
||||
"memoryDebugPanel.requests.now": "agora",
|
||||
"memoryDebugPanel.requests.noSamples": "Nenhuma solicitação registrada. Mantenha este painel aberto para registrar a atividade de fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Solicitações fetch em curso ao longo do tempo, pico {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "últimos {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Percentis de idade das solicitações em curso (p50, p90, p99, máx) ao longo do tempo",
|
||||
"memoryDebugPanel.common.idle": "inativo",
|
||||
"memoryDebugPanel.common.live": "ao vivo",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
@@ -3111,7 +3122,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
"quota.window.premiumInteractions": "Créditos de IA",
|
||||
'chat.workStatus.ariaLabel': 'Status do trabalho',
|
||||
'chat.workStatus.context.label': 'Contexto',
|
||||
'chat.workStatus.cost.breakdown': "Sessão {session} · Subagentes {subagents}",
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"multirun.launcher.attachments.attach": "Прикріпити",
|
||||
"multirun.launcher.attachments.tooltip": "Ті самі файли буде надіслано в усі запуски",
|
||||
"multirun.launcher.models.label": "Моделі",
|
||||
"multirun.launcher.models.info": "Вибрати моделі 2-{max}. Ту саму модель можна додавати кілька разів.",
|
||||
"multirun.launcher.models.info": "Вибрати 2 або більше моделей. Ту саму модель можна додавати кілька разів.",
|
||||
"multirun.launcher.toast.fileTooLarge": "Файл \"{fileName}\" завеликий (макс. 10 МБ)",
|
||||
"multirun.launcher.toast.attachFailed": "Не вдалося вкласти \"{fileName}\"",
|
||||
"multirun.launcher.toast.attachedSingle": "Прикріплено файл: {count}",
|
||||
@@ -1936,8 +1936,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"session.newWorktree.noMatchingBranches": "Немає відповідних гілок",
|
||||
"session.newWorktree.localBranches": "Локальні гілки",
|
||||
"session.newWorktree.remoteBranches": "Віддалені гілки",
|
||||
"session.newWorktree.otherLocalBranches": "Інші локальні гілки",
|
||||
"session.newWorktree.otherRemoteBranches": "Інші віддалені гілки",
|
||||
"session.newWorktree.branchName": "Назва гілки",
|
||||
"session.newWorktree.branchNamePlaceholder": "feature/my-awesome-feature",
|
||||
"session.newWorktree.actions.change": "Змінити",
|
||||
@@ -2268,6 +2266,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.attachmentsTooLarge": "Вкладені файли завеликі для надсилання. Спробуйте зменшити кількість або розмір зображень.",
|
||||
"chat.chatInput.toast.sendAttachmentsFailed": "Не вдалося надіслати вкладення. Спробуйте зменшити кількість файлів або зображень.",
|
||||
"chat.chatInput.toast.messageSendFailed": "Не вдалося надіслати повідомлення. Вкладення відновлено.",
|
||||
"chat.chatInput.toast.noModelSelected": "Виберіть постачальника та модель перед надсиланням.",
|
||||
"chat.chatInput.toast.clipboardAttachFailed": "Не вдалося вкласти зображення з буфера обміну",
|
||||
"chat.chatInput.toast.addedFileMentions": "Додано згадки файлів {count}",
|
||||
"chat.chatInput.toast.attachFileFailed": "Не вдалося прикріпити файл",
|
||||
@@ -2317,6 +2316,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.toolPart.showRawJson": "Показати сирий JSON",
|
||||
"chat.toolPart.showFormattedJson": "Показати форматований JSON",
|
||||
"chat.toolPart.showNavigableJson": "Показати навігаційний JSON",
|
||||
"chat.toolPart.openFile": "Відкрити файл",
|
||||
"chat.toolPart.openFileAtFirstChange": "Відкрити файл на першій зміні",
|
||||
"chat.toolPart.openFileDiff": "Відкрити diff файлу",
|
||||
"chat.toolPart.copyOutput": "Скопіювати вивід",
|
||||
@@ -2976,6 +2976,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Панель налагодження",
|
||||
"memoryDebugPanel.tabs.memory": "Пам'ять",
|
||||
"memoryDebugPanel.tabs.streaming": "Потокове передавання",
|
||||
"memoryDebugPanel.tabs.requests": "Запити",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Сесії в пам'яті",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Потокові показники інтерфейсу користувача",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Метрики мосту VS Code",
|
||||
@@ -3013,6 +3014,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "Потокове налагодження JSON скопійовано",
|
||||
"memoryDebugPanel.streaming.copy.failed": "Не вдалося скопіювати JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Копіювання експортує метрики потокового інтерфейсу користувача та VS Code як JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "Виконуються",
|
||||
"memoryDebugPanel.requests.peak": "Пік",
|
||||
"memoryDebugPanel.requests.duration": "Тривалість",
|
||||
"memoryDebugPanel.requests.totalRequests": "Усього запитів",
|
||||
"memoryDebugPanel.requests.tracking": "Відстеження",
|
||||
"memoryDebugPanel.requests.now": "зараз",
|
||||
"memoryDebugPanel.requests.noSamples": "Запитів ще немає. Тримайте цю панель відкритою, щоб фіксувати активність fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Запити fetch у виконанні з часом, пік {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "останні {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Перцентилі віку запитів у виконанні (p50, p90, p99, max) з часом",
|
||||
"memoryDebugPanel.common.idle": "очікування",
|
||||
"memoryDebugPanel.common.live": "live",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
@@ -3111,7 +3122,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
"quota.window.premiumInteractions": "Кредити ШІ",
|
||||
'chat.workStatus.ariaLabel': 'Стан роботи',
|
||||
'chat.workStatus.context.label': 'Контекст',
|
||||
'chat.workStatus.cost.breakdown': "Сеанс {session} · Субагенти {subagents}",
|
||||
|
||||
@@ -384,7 +384,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'multirun.launcher.attachments.attach': '附加',
|
||||
'multirun.launcher.attachments.tooltip': '相同文件会发送到所有运行',
|
||||
'multirun.launcher.models.label': '模型',
|
||||
'multirun.launcher.models.info': '选择 2-{max} 个模型。同一模型可重复添加。',
|
||||
'multirun.launcher.models.info': '选择 2 个或更多模型。同一模型可重复添加。',
|
||||
'multirun.launcher.toast.fileTooLarge': '文件“{fileName}”过大(最大 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': '附加“{fileName}”失败',
|
||||
'multirun.launcher.toast.attachedSingle': '已附加 {count} 个文件',
|
||||
@@ -1924,8 +1924,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'session.newWorktree.noMatchingBranches': '没有匹配分支',
|
||||
'session.newWorktree.localBranches': '本地分支',
|
||||
'session.newWorktree.remoteBranches': '远程分支',
|
||||
'session.newWorktree.otherLocalBranches': '其他本地分支',
|
||||
'session.newWorktree.otherRemoteBranches': '其他远程分支',
|
||||
'session.newWorktree.branchName': '分支名',
|
||||
'session.newWorktree.branchNamePlaceholder': 'feature/my-awesome-feature',
|
||||
'session.newWorktree.actions.change': '更改',
|
||||
@@ -2268,6 +2266,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': '附件过大,无法发送。请减少图片数量或大小。',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': '发送附件失败。请尝试更少文件或更小图片。',
|
||||
'chat.chatInput.toast.messageSendFailed': '消息发送失败,附件已恢复。',
|
||||
'chat.chatInput.toast.noModelSelected': '发送前请先选择提供商和模型。',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': '从剪贴板附加图片失败',
|
||||
'chat.chatInput.toast.addedFileMentions': '已添加 {count} 个文件提及',
|
||||
'chat.chatInput.toast.attachFileFailed': '附加文件失败',
|
||||
@@ -2317,6 +2316,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.toolPart.showRawJson': '显示原始 JSON',
|
||||
'chat.toolPart.showFormattedJson': '显示格式化 JSON',
|
||||
'chat.toolPart.showNavigableJson': '显示可导航 JSON',
|
||||
'chat.toolPart.openFile': '打开文件',
|
||||
'chat.toolPart.openFileAtFirstChange': '在首次更改处打开文件',
|
||||
'chat.toolPart.openFileDiff': '打开文件差异',
|
||||
'chat.toolPart.copyOutput': '复制输出',
|
||||
@@ -2976,6 +2976,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '调试面板',
|
||||
'memoryDebugPanel.tabs.memory': '内存',
|
||||
'memoryDebugPanel.tabs.streaming': '流式',
|
||||
'memoryDebugPanel.tabs.requests': '请求',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '内存中的会话',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 流式指标',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 桥接指标',
|
||||
@@ -3013,6 +3014,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '流式调试 JSON 已复制',
|
||||
'memoryDebugPanel.streaming.copy.failed': '复制 JSON 失败',
|
||||
'memoryDebugPanel.streaming.copy.hint': '复制会导出 UI 与 VS Code 的流式指标 JSON',
|
||||
'memoryDebugPanel.requests.inFlight': '进行中',
|
||||
'memoryDebugPanel.requests.peak': '峰值',
|
||||
'memoryDebugPanel.requests.duration': '时长',
|
||||
'memoryDebugPanel.requests.totalRequests': '请求总数',
|
||||
'memoryDebugPanel.requests.tracking': '跟踪',
|
||||
'memoryDebugPanel.requests.now': '当前',
|
||||
'memoryDebugPanel.requests.noSamples': '尚未记录请求。保持此面板打开以记录 fetch 活动。',
|
||||
'memoryDebugPanel.requests.chartLabel': '随时间变化的进行中 fetch 请求,峰值 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '最近 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '进行中请求年龄百分位(p50、p90、p99、最大值)随时间的变化',
|
||||
'memoryDebugPanel.common.idle': '空闲',
|
||||
'memoryDebugPanel.common.live': '实时',
|
||||
'memoryDebugPanel.common.notAvailable': '无',
|
||||
@@ -3111,7 +3122,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
'quota.window.premiumInteractions': 'AI 点数',
|
||||
'chat.workStatus.ariaLabel': '工作状态',
|
||||
'chat.workStatus.context.label': '上下文',
|
||||
'chat.workStatus.cost.breakdown': '会话 {session} · 子智能体 {subagents}',
|
||||
|
||||
@@ -397,7 +397,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'multirun.launcher.attachments.attach': '附加',
|
||||
'multirun.launcher.attachments.tooltip': '相同檔案會傳送到所有執行',
|
||||
'multirun.launcher.models.label': '模型',
|
||||
'multirun.launcher.models.info': '選擇 2-{max} 個模型。同一模型可重複加入。',
|
||||
'multirun.launcher.models.info': '選擇 2 個或更多模型。同一模型可重複加入。',
|
||||
'multirun.launcher.toast.fileTooLarge': '檔案「{fileName}」過大(最大 10MB)',
|
||||
'multirun.launcher.toast.attachFailed': '附加「{fileName}」失敗',
|
||||
'multirun.launcher.toast.attachedSingle': '已附加 {count} 個檔案',
|
||||
@@ -1928,8 +1928,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'session.newWorktree.noMatchingBranches': '沒有符合分支',
|
||||
'session.newWorktree.localBranches': '本地分支',
|
||||
'session.newWorktree.remoteBranches': '遠端分支',
|
||||
'session.newWorktree.otherLocalBranches': '其他本地分支',
|
||||
'session.newWorktree.otherRemoteBranches': '其他遠端分支',
|
||||
'session.newWorktree.branchName': '分支名稱',
|
||||
'session.newWorktree.branchNamePlaceholder': 'feature/my-awesome-feature',
|
||||
'session.newWorktree.actions.change': '變更',
|
||||
@@ -2272,6 +2270,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.attachmentsTooLarge': '附件過大,無法傳送。請減少圖片數量或大小。',
|
||||
'chat.chatInput.toast.sendAttachmentsFailed': '傳送附件失敗。請嘗試更少檔案或更小圖片。',
|
||||
'chat.chatInput.toast.messageSendFailed': '訊息傳送失敗,附件已恢復。',
|
||||
'chat.chatInput.toast.noModelSelected': '傳送前請先選擇提供者與模型。',
|
||||
'chat.chatInput.toast.clipboardAttachFailed': '從剪貼簿附加圖片失敗',
|
||||
'chat.chatInput.toast.addedFileMentions': '已加入 {count} 個檔案提及',
|
||||
'chat.chatInput.toast.attachFileFailed': '附加檔案失敗',
|
||||
@@ -2321,6 +2320,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.toolPart.showRawJson': '顯示原始 JSON',
|
||||
'chat.toolPart.showFormattedJson': '顯示格式化 JSON',
|
||||
'chat.toolPart.showNavigableJson': '顯示可導覽 JSON',
|
||||
'chat.toolPart.openFile': '開啟檔案',
|
||||
'chat.toolPart.openFileAtFirstChange': '在首次變更處開啟檔案',
|
||||
'chat.toolPart.openFileDiff': '開啟檔案差異',
|
||||
'chat.toolPart.copyOutput': '複製輸出',
|
||||
@@ -2973,6 +2973,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '偵錯面板',
|
||||
'memoryDebugPanel.tabs.memory': '記憶體',
|
||||
'memoryDebugPanel.tabs.streaming': '串流',
|
||||
'memoryDebugPanel.tabs.requests': '請求',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '記憶體中的會話',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 串流指標',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 橋接指標',
|
||||
@@ -3010,6 +3011,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '串流偵錯 JSON 已複製',
|
||||
'memoryDebugPanel.streaming.copy.failed': '複製 JSON 失敗',
|
||||
'memoryDebugPanel.streaming.copy.hint': '複製會匯出 UI 與 VS Code 的串流指標 JSON',
|
||||
'memoryDebugPanel.requests.inFlight': '進行中',
|
||||
'memoryDebugPanel.requests.peak': '峰值',
|
||||
'memoryDebugPanel.requests.duration': '時長',
|
||||
'memoryDebugPanel.requests.totalRequests': '請求總數',
|
||||
'memoryDebugPanel.requests.tracking': '追蹤',
|
||||
'memoryDebugPanel.requests.now': '目前',
|
||||
'memoryDebugPanel.requests.noSamples': '尚未記錄請求。保持此面板開啟以記錄 fetch 活動。',
|
||||
'memoryDebugPanel.requests.chartLabel': '隨時間變化的進行中 fetch 請求,峰值 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '最近 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '進行中請求年齡百分位(p50、p90、p99、最大值)隨時間的變化',
|
||||
'memoryDebugPanel.common.idle': '閒置',
|
||||
'memoryDebugPanel.common.live': '即時',
|
||||
'memoryDebugPanel.common.notAvailable': '無',
|
||||
@@ -3110,7 +3121,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
'quota.window.premiumInteractions': 'AI 點數',
|
||||
'chat.workStatus.ariaLabel': '工作狀態',
|
||||
'chat.workStatus.context.label': '上下文',
|
||||
'chat.workStatus.cost.breakdown': '工作階段 {session} · 子 Agent {subagents}',
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import { flattenAssistantTextParts, flattenUserTextParts } from './messageText';
|
||||
|
||||
// Regression tests for https://github.com/openchamber/openchamber/issues/2867
|
||||
//
|
||||
// `flattenAssistantTextParts` used to collapse every blank line into a single
|
||||
// `\n`. Markdown block structure (paragraphs, lists, fenced code blocks)
|
||||
// requires a blank line (`\n\n`); a single `\n` is a CommonMark soft break.
|
||||
// `ChatMessage.tsx`'s `handleCopyMessage` feeds the flattened string into
|
||||
// `copyMarkdownToClipboard`, which writes it to `text/plain`, `text/markdown`
|
||||
// and its markdown-rendered HTML into `text/html`.
|
||||
|
||||
const basePart = (overrides: Record<string, unknown>): Part =>
|
||||
({
|
||||
id: 'p1',
|
||||
sessionID: 's',
|
||||
messageID: 'm',
|
||||
type: 'text',
|
||||
text: '',
|
||||
...overrides,
|
||||
}) as Part;
|
||||
|
||||
const makeParts = (texts: string[]): Part[] =>
|
||||
texts.map((text, index) => basePart({ id: `p${index}`, text }));
|
||||
|
||||
const makeUserParts = (
|
||||
entries: Array<{ text?: string; shellAction?: { output?: unknown; command?: unknown } }>,
|
||||
): Part[] =>
|
||||
entries.map((entry, index) =>
|
||||
basePart({ id: `u${index}`, text: entry.text ?? '', shellAction: entry.shellAction }),
|
||||
);
|
||||
|
||||
describe('flattenAssistantTextParts', () => {
|
||||
const parts = makeParts([
|
||||
'第一段',
|
||||
'第二段',
|
||||
'```js\nconsole.log(1)\n```',
|
||||
'第三段',
|
||||
'- item 1\n- item 2',
|
||||
]);
|
||||
|
||||
test('blank lines between paragraphs/code blocks/lists are preserved', () => {
|
||||
expect(flattenAssistantTextParts(parts)).toBe(
|
||||
'第一段\n\n第二段\n\n```js\nconsole.log(1)\n```\n\n第三段\n\n- item 1\n- item 2',
|
||||
);
|
||||
});
|
||||
|
||||
test('a code fence is not glued to the following paragraph', () => {
|
||||
const flattened = flattenAssistantTextParts(parts);
|
||||
expect(flattened).not.toContain('```\n第三段');
|
||||
expect(flattened).toContain('```\n\n第三段');
|
||||
});
|
||||
|
||||
test('list items keep single newlines inside their part', () => {
|
||||
expect(flattenAssistantTextParts(parts)).toContain('\n\n- item 1\n- item 2');
|
||||
});
|
||||
|
||||
test('internal blank-line runs are preserved', () => {
|
||||
const text = 'a\n\n\n\nb\n \n \nd';
|
||||
expect(flattenAssistantTextParts(makeParts([text]))).toBe(text);
|
||||
});
|
||||
|
||||
test('multiple blank lines inside a fenced code block are preserved', () => {
|
||||
const fenced = '```js\na\n\n\nb\n```';
|
||||
expect(flattenAssistantTextParts(makeParts([fenced]))).toBe(fenced);
|
||||
});
|
||||
|
||||
test('part boundaries produce block separators', () => {
|
||||
expect(flattenAssistantTextParts(makeParts(['first', 'second']))).toBe('first\n\nsecond');
|
||||
});
|
||||
|
||||
test('empty and whitespace-only parts are dropped', () => {
|
||||
expect(flattenAssistantTextParts([])).toBe('');
|
||||
expect(flattenAssistantTextParts(makeParts(['', ' ', '\n']))).toBe('');
|
||||
});
|
||||
|
||||
test('single part without blank lines is returned unchanged', () => {
|
||||
const single = 'only line\nsecond line';
|
||||
expect(flattenAssistantTextParts(makeParts([single]))).toBe(single);
|
||||
});
|
||||
|
||||
test('non-text parts are ignored', () => {
|
||||
const partsWithTool: Part[] = [
|
||||
...makeParts(['before']),
|
||||
{ id: 't1', sessionID: 's', messageID: 'm', type: 'tool', tool: 'bash' } as Part,
|
||||
...makeParts(['after']),
|
||||
];
|
||||
expect(flattenAssistantTextParts(partsWithTool)).toBe('before\n\nafter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenUserTextParts', () => {
|
||||
test('plain text parts keep blank-line block separators', () => {
|
||||
const parts = makeUserParts([{ text: '第一段\n\n\n第二段' }, { text: '下一段' }]);
|
||||
expect(flattenUserTextParts(parts)).toBe('第一段\n\n\n第二段\n\n下一段');
|
||||
});
|
||||
|
||||
test('shell outputs win over other content and are joined with blank lines', () => {
|
||||
const parts = makeUserParts([
|
||||
{ text: 'note', shellAction: { command: 'ls -la' } },
|
||||
{ text: '', shellAction: { output: ' file-a\nfile-b ' } },
|
||||
{ text: '', shellAction: { output: 'done' } },
|
||||
]);
|
||||
expect(flattenUserTextParts(parts)).toBe('file-a\nfile-b\n\ndone');
|
||||
});
|
||||
|
||||
test('shell commands fall back to a single-newline command list', () => {
|
||||
const parts = makeUserParts([
|
||||
{ shellAction: { command: ' bun install ' } },
|
||||
{ shellAction: { command: 'bun test' } },
|
||||
{ text: 'ignored when commands exist' },
|
||||
]);
|
||||
expect(flattenUserTextParts(parts)).toBe('bun install\nbun test');
|
||||
});
|
||||
|
||||
test('returns empty string for parts without text', () => {
|
||||
expect(flattenUserTextParts([])).toBe('');
|
||||
expect(flattenUserTextParts(makeUserParts([{ text: ' ' }]))).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
|
||||
type TextLikePart = Part & { text?: string; content?: string };
|
||||
type UserTextPart = Part & { text?: string; content?: string; shellAction?: { output?: unknown; command?: unknown } };
|
||||
|
||||
export const flattenAssistantTextParts = (parts: Part[]): string => {
|
||||
const textParts = parts
|
||||
@@ -8,8 +9,36 @@ export const flattenAssistantTextParts = (parts: Part[]): string => {
|
||||
.map((part) => (part.text || part.content || '').trim())
|
||||
.filter((text) => text.length > 0);
|
||||
|
||||
const combined = textParts.join('\n');
|
||||
return combined.replace(/\n\s*\n+/g, '\n');
|
||||
return textParts.join('\n\n');
|
||||
};
|
||||
|
||||
export const flattenUserTextParts = (parts: Part[]): string => {
|
||||
const textParts = parts.filter((part): part is UserTextPart => part?.type === 'text');
|
||||
|
||||
const shellOutputs = textParts
|
||||
.map((part) => {
|
||||
const output = part.shellAction?.output;
|
||||
return typeof output === 'string' ? output.trim() : '';
|
||||
})
|
||||
.filter((output) => output.length > 0);
|
||||
if (shellOutputs.length > 0) {
|
||||
return shellOutputs.join('\n\n');
|
||||
}
|
||||
|
||||
const shellCommands = textParts
|
||||
.map((part) => {
|
||||
const command = part.shellAction?.command;
|
||||
return typeof command === 'string' ? command.trim() : '';
|
||||
})
|
||||
.filter((command) => command.length > 0);
|
||||
if (shellCommands.length > 0) {
|
||||
return shellCommands.join('\n');
|
||||
}
|
||||
|
||||
const plainTexts = textParts
|
||||
.map((part) => (part.text || part.content || '').trim())
|
||||
.filter((text) => text.length > 0);
|
||||
return plainTexts.join('\n\n');
|
||||
};
|
||||
|
||||
export const suggestPlanTitleFromText = (text: string): string => {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { createPlanSaveQueue } from './planSaveQueue';
|
||||
|
||||
type Deferred = { promise: Promise<void>; resolve: () => void; reject: () => void };
|
||||
|
||||
const deferred = (): Deferred => {
|
||||
let resolve!: () => void;
|
||||
let reject!: () => void;
|
||||
const promise = new Promise<void>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
describe('planSaveQueue', () => {
|
||||
test('runs writes for one document in schedule order even when they resolve out of order', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
const order: string[] = [];
|
||||
const first = deferred();
|
||||
const second = deferred();
|
||||
|
||||
const firstDone = queue.schedule('doc', 1, async () => {
|
||||
await first.promise;
|
||||
order.push('first');
|
||||
});
|
||||
const secondDone = queue.schedule('doc', 2, async () => {
|
||||
order.push('second');
|
||||
});
|
||||
|
||||
// Second started only after first settles, regardless of timing.
|
||||
first.resolve();
|
||||
await firstDone;
|
||||
second.resolve();
|
||||
await secondDone;
|
||||
|
||||
expect(order).toEqual(['first', 'second']);
|
||||
});
|
||||
|
||||
test('skips a revision at or below the last queued revision for the same document', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
let writes = 0;
|
||||
|
||||
await queue.schedule('doc', 3, async () => {
|
||||
writes += 1;
|
||||
});
|
||||
await queue.schedule('doc', 3, async () => {
|
||||
writes += 1;
|
||||
});
|
||||
await queue.schedule('doc', 2, async () => {
|
||||
writes += 1;
|
||||
});
|
||||
|
||||
expect(writes).toBe(1);
|
||||
});
|
||||
|
||||
test('never lets a write for one document block another document', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
const blocked = deferred();
|
||||
|
||||
const blockedDone = queue.schedule('a', 1, async () => {
|
||||
await blocked.promise;
|
||||
});
|
||||
let otherRan = false;
|
||||
await queue.schedule('b', 1, async () => {
|
||||
otherRan = true;
|
||||
});
|
||||
|
||||
expect(otherRan).toBe(true);
|
||||
blocked.resolve();
|
||||
await blockedDone;
|
||||
});
|
||||
|
||||
test('pendingFor waits for the outstanding chain of that document only', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
const slow = deferred();
|
||||
let slowSettled = false;
|
||||
|
||||
void queue.schedule('a', 1, async () => {
|
||||
await slow.promise;
|
||||
slowSettled = true;
|
||||
});
|
||||
await queue.schedule('b', 1, async () => {});
|
||||
|
||||
await queue.pendingFor('b');
|
||||
expect(slowSettled).toBe(false);
|
||||
|
||||
slow.resolve();
|
||||
await queue.pendingFor('a');
|
||||
expect(slowSettled).toBe(true);
|
||||
});
|
||||
|
||||
test('reset clears the revision watermark so a reloaded document can save again', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
let writes = 0;
|
||||
|
||||
await queue.schedule('doc', 5, async () => {
|
||||
writes += 1;
|
||||
});
|
||||
queue.reset('doc');
|
||||
await queue.schedule('doc', 1, async () => {
|
||||
writes += 1;
|
||||
});
|
||||
|
||||
expect(writes).toBe(2);
|
||||
});
|
||||
|
||||
test('a failed write does not poison the chain for later writes', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
|
||||
const failing = queue.schedule('doc', 1, async () => {
|
||||
throw new Error('write failed');
|
||||
});
|
||||
let secondRan = false;
|
||||
const second = queue.schedule('doc', 2, async () => {
|
||||
secondRan = true;
|
||||
});
|
||||
|
||||
await expect(failing).rejects.toThrow('write failed');
|
||||
await second;
|
||||
expect(secondRan).toBe(true);
|
||||
await queue.pendingFor('doc');
|
||||
});
|
||||
|
||||
test('allows the same revision to retry after its write fails', async () => {
|
||||
const queue = createPlanSaveQueue();
|
||||
let attempts = 0;
|
||||
|
||||
const failing = queue.schedule('doc', 1, async () => {
|
||||
attempts += 1;
|
||||
throw new Error('write failed');
|
||||
});
|
||||
await expect(failing).rejects.toThrow('write failed');
|
||||
|
||||
await queue.schedule('doc', 1, async () => {
|
||||
attempts += 1;
|
||||
});
|
||||
|
||||
expect(attempts).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Write queue for open plan documents.
|
||||
*
|
||||
* Debounced autosave and close-time flushes must reach the disk in edit order,
|
||||
* and a document re-opened while its own write is still in flight must read
|
||||
* the post-write state, not race it. The queue serializes writes per logical
|
||||
* document key and deduplicates revisions so a flush of revision N can never
|
||||
* run behind, or twice behind, a debounced save of the same revision.
|
||||
*/
|
||||
|
||||
interface PlanSaveQueue {
|
||||
/**
|
||||
* Queue one write for `key`. Writes for the same key run in schedule order;
|
||||
* writes for different keys never block each other. A revision at or below
|
||||
* the last queued revision for that key is skipped — the queued write
|
||||
* already carries newer content — and the returned promise tracks the
|
||||
* outstanding chain so callers can still await it.
|
||||
*/
|
||||
schedule: (key: string, revision: number, write: () => Promise<void>) => Promise<void>;
|
||||
/** Resolves when every write queued for `key` has settled. */
|
||||
pendingFor: (key: string) => Promise<void>;
|
||||
/**
|
||||
* Forgets the revision watermark for `key`. Call when a document is freshly
|
||||
* loaded: its revision counter restarts, and stale watermarks from a
|
||||
* previous open must not swallow the first real edit.
|
||||
*/
|
||||
reset: (key: string) => void;
|
||||
}
|
||||
|
||||
export const createPlanSaveQueue = (): PlanSaveQueue => {
|
||||
const chains = new Map<string, Promise<void>>();
|
||||
const lastRevision = new Map<string, number>();
|
||||
|
||||
return {
|
||||
schedule: (key, revision, write) => {
|
||||
if (revision <= (lastRevision.get(key) ?? Number.NEGATIVE_INFINITY)) {
|
||||
return chains.get(key) ?? Promise.resolve();
|
||||
}
|
||||
lastRevision.set(key, revision);
|
||||
const previous = chains.get(key) ?? Promise.resolve();
|
||||
// A failed write must not poison the chain: the next write for this
|
||||
// document is still safe to attempt, and error surfacing belongs to the
|
||||
// caller that owns UI state.
|
||||
const next = previous.then(write, write);
|
||||
chains.set(key, next.catch(() => {
|
||||
// Keep newer queued revisions deduplicated, but let the caller retry
|
||||
// this exact revision after its write has failed.
|
||||
if (lastRevision.get(key) === revision) {
|
||||
lastRevision.delete(key);
|
||||
}
|
||||
}));
|
||||
return next;
|
||||
},
|
||||
pendingFor: async (key) => {
|
||||
await chains.get(key);
|
||||
},
|
||||
reset: (key) => {
|
||||
lastRevision.delete(key);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -57,6 +57,17 @@ export interface ProjectRef {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A saved project plan plus the project that owns it, carried as one value so
|
||||
* a viewer can never end up with a plan id whose owner it has to guess.
|
||||
* PlanView resolves no owner on its own: the panel (or the persisted tab,
|
||||
* or the mobile surface) that opened the plan knows the owner exactly.
|
||||
*/
|
||||
export interface SavedProjectPlanTarget {
|
||||
projectRef: ProjectRef;
|
||||
planId: string;
|
||||
}
|
||||
|
||||
export const PROJECT_NOTE_BODY_MAX_LENGTH = 3000;
|
||||
export const PROJECT_TODO_TEXT_MAX_LENGTH = 120;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { clampPercent, formatPercent } from './utils';
|
||||
import { clampPercent, formatPercent, formatWindowLabel } from './utils';
|
||||
|
||||
describe('quota utils', () => {
|
||||
test('treats non-finite percentages as missing', () => {
|
||||
@@ -10,4 +10,9 @@ describe('quota utils', () => {
|
||||
expect(formatPercent(Infinity)).toBe('-');
|
||||
expect(formatPercent(-Infinity)).toBe('-');
|
||||
});
|
||||
|
||||
test('labels Copilot usage as AI Credits without changing generic premium usage', () => {
|
||||
expect(formatWindowLabel('premium')).toBe('Premium Interactions');
|
||||
expect(formatWindowLabel('premium_interactions')).toBe('AI Credits');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
withBtwSessionLink,
|
||||
withBtwSessionMarker,
|
||||
withoutBtwSessionLink,
|
||||
wasPromotedBtwSession,
|
||||
withoutBtwSessionMarker,
|
||||
} from './sessionBtwMetadata';
|
||||
|
||||
@@ -64,11 +65,20 @@ describe('fork marker', () => {
|
||||
expect(getBtwBoundaryMessageID(review)).toBeNull();
|
||||
});
|
||||
|
||||
test('withoutBtwSessionMarker strips the marker and keeps other keys', () => {
|
||||
test('withoutBtwSessionMarker strips the marker, keeps other keys, and records the promotion', () => {
|
||||
const marked = { openchamber: { kind: 'btw', originalSessionID: 'parent-1', btwBoundaryMessageID: 'msg-9', btwSessionID: 'nested' } };
|
||||
expect(withoutBtwSessionMarker(marked)).toEqual({ openchamber: { btwSessionID: 'nested' } });
|
||||
expect(withoutBtwSessionMarker({ openchamber: { kind: 'btw', originalSessionID: 'parent-1' } })).toEqual({});
|
||||
expect(withoutBtwSessionMarker(marked)).toEqual({ openchamber: { btwSessionID: 'nested', btwPromoted: true } });
|
||||
expect(withoutBtwSessionMarker({ openchamber: { kind: 'btw', originalSessionID: 'parent-1' } })).toEqual({ openchamber: { btwPromoted: true } });
|
||||
const plain = { openchamber: { kind: 'review' } };
|
||||
expect(withoutBtwSessionMarker(plain)).toBe(plain);
|
||||
});
|
||||
|
||||
test('wasPromotedBtwSession only reports a session that went through promotion', () => {
|
||||
expect(wasPromotedBtwSession(sessionWith({ openchamber: { btwPromoted: true } }))).toBe(true);
|
||||
// Still a live btw fork: the boundary applies, the notice must not.
|
||||
expect(wasPromotedBtwSession(sessionWith({ openchamber: { kind: 'btw', originalSessionID: 'p-1' } }))).toBe(false);
|
||||
expect(wasPromotedBtwSession(sessionWith({ openchamber: {} }))).toBe(false);
|
||||
expect(wasPromotedBtwSession(sessionWith(undefined))).toBe(false);
|
||||
expect(wasPromotedBtwSession(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ type BtwMetadata = {
|
||||
originalSessionID?: string;
|
||||
btwSessionID?: string;
|
||||
btwBoundaryMessageID?: string;
|
||||
btwPromoted?: boolean;
|
||||
};
|
||||
|
||||
const getOpenChamberMetadata = (metadata: SessionMetadataRecord): BtwMetadata => {
|
||||
@@ -39,6 +40,18 @@ const nonEmpty = (value: string | undefined): string | null =>
|
||||
export const getBtwSessionID = (session: Session | null | undefined): string | null =>
|
||||
nonEmpty(getOpenChamberMetadata(getSessionMetadata(session)).btwSessionID);
|
||||
|
||||
/**
|
||||
* The session was once a btw fork and was promoted to a normal session.
|
||||
*
|
||||
* Its transcript still contains the btw boundary instruction on every message
|
||||
* sent while it was a side conversation, and there is no API to remove a
|
||||
* message part after the fact. The flag lets the composer send a notice that
|
||||
* those constraints have been lifted, so they cannot keep steering a session
|
||||
* that is no longer a side conversation.
|
||||
*/
|
||||
export const wasPromotedBtwSession = (session: Session | null | undefined): boolean =>
|
||||
getOpenChamberMetadata(getSessionMetadata(session)).btwPromoted === true;
|
||||
|
||||
export const isBtwSession = (session: Session | null | undefined): boolean =>
|
||||
getOpenChamberMetadata(getSessionMetadata(session)).kind === 'btw'
|
||||
&& Boolean(getBtwOriginalSessionID(session));
|
||||
@@ -84,7 +97,13 @@ export const withBtwSessionMarker = (
|
||||
return { ...metadata, openchamber };
|
||||
};
|
||||
|
||||
/** Remove the btw marker so a promoted fork becomes a plain session. */
|
||||
/**
|
||||
* Remove the btw marker so a promoted fork becomes a plain session.
|
||||
*
|
||||
* `btwPromoted` replaces it rather than leaving nothing behind: the btw
|
||||
* boundary instructions stay in the transcript forever, so the session has to
|
||||
* remain distinguishable from one that was never a side conversation.
|
||||
*/
|
||||
export const withoutBtwSessionMarker = (metadata: SessionMetadataRecord): SessionMetadataRecord => {
|
||||
const openchamber = getOpenChamberMetadata(metadata);
|
||||
if (openchamber.kind !== 'btw') return metadata;
|
||||
@@ -92,13 +111,8 @@ export const withoutBtwSessionMarker = (metadata: SessionMetadataRecord): Sessio
|
||||
delete rest.kind;
|
||||
delete rest.originalSessionID;
|
||||
delete rest.btwBoundaryMessageID;
|
||||
const next: SessionMetadataRecord = { ...metadata };
|
||||
if (Object.keys(rest).length > 0) {
|
||||
next.openchamber = rest;
|
||||
} else {
|
||||
delete next.openchamber;
|
||||
}
|
||||
return next;
|
||||
rest.btwPromoted = true;
|
||||
return { ...metadata, openchamber: rest };
|
||||
};
|
||||
|
||||
/** Unlink the parent, but only if it still points at this fork. */
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#A277FF",
|
||||
"linkHover": "#F694FF",
|
||||
"inlineCode": "#61FFCA",
|
||||
"inlineCodeBackground": "#1A1921",
|
||||
"inlineCodeBackground": "#222128",
|
||||
"blockquote": "#6D6D6D",
|
||||
"blockquoteBorder": "#2D2B38",
|
||||
"listMarker": "#A277FF99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#2D2640",
|
||||
"link": "#A277FF",
|
||||
"linkHover": "#C17AC8",
|
||||
"inlineCode": "#40BF7A",
|
||||
"inlineCodeBackground": "#EFE8FC",
|
||||
"inlineCode": "#00732E",
|
||||
"inlineCodeBackground": "#E8E3F2",
|
||||
"blockquote": "#6D6D6D",
|
||||
"blockquoteBorder": "#E0D6F2",
|
||||
"listMarker": "#A277FF99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#66C6F1",
|
||||
"linkHover": "#3FB7E3",
|
||||
"inlineCode": "#B1C74A",
|
||||
"inlineCodeBackground": "#161d23",
|
||||
"inlineCodeBackground": "#1C2126",
|
||||
"blockquote": "#E4A75C",
|
||||
"blockquoteBorder": "#2B3440",
|
||||
"listMarker": "#3FB7E399"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#394049",
|
||||
"link": "#2F9BCE",
|
||||
"linkHover": "#4AA8C8",
|
||||
"inlineCode": "#7FAD00",
|
||||
"inlineCodeBackground": "#FCF9F3",
|
||||
"inlineCode": "#497700",
|
||||
"inlineCodeBackground": "#F0EDE7",
|
||||
"blockquote": "#ED982E",
|
||||
"blockquoteBorder": "#E6DDCF",
|
||||
"listMarker": "#4AA8C899"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#33B1FF",
|
||||
"linkHover": "#78A9FF",
|
||||
"inlineCode": "#42BE65",
|
||||
"inlineCodeBackground": "#1e1e1e",
|
||||
"inlineCodeBackground": "#232323",
|
||||
"blockquote": "#8D8D8D",
|
||||
"blockquoteBorder": "#393939",
|
||||
"listMarker": "#33B1FF99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#161616",
|
||||
"link": "#0072C3",
|
||||
"linkHover": "#0043CE",
|
||||
"inlineCode": "#198038",
|
||||
"inlineCodeBackground": "#F4F4F4",
|
||||
"inlineCode": "#00661E",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#525252",
|
||||
"blockquoteBorder": "#DCDCDC",
|
||||
"listMarker": "#0072C399"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#89DCEB",
|
||||
"linkHover": "#B4BEFE",
|
||||
"inlineCode": "#A6E3A1",
|
||||
"inlineCodeBackground": "#2d2a42",
|
||||
"inlineCodeBackground": "#2B2B3B",
|
||||
"blockquote": "#F9E2AF",
|
||||
"blockquoteBorder": "#35324A",
|
||||
"listMarker": "#B4BEFE99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#2e314a",
|
||||
"link": "#04A5E5",
|
||||
"linkHover": "#7287FD",
|
||||
"inlineCode": "#40A02B",
|
||||
"inlineCodeBackground": "#f6eeec",
|
||||
"inlineCode": "#1A7A05",
|
||||
"inlineCodeBackground": "#F2E9E7",
|
||||
"blockquote": "#DF8E1D",
|
||||
"blockquoteBorder": "#E0CFD3",
|
||||
"listMarker": "#7287FD99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#8BE9FD",
|
||||
"linkHover": "#BD93F9",
|
||||
"inlineCode": "#4aeb72",
|
||||
"inlineCodeBackground": "#202132",
|
||||
"inlineCodeBackground": "#21222C",
|
||||
"blockquote": "#FFB86C",
|
||||
"blockquoteBorder": "#2D2F3C",
|
||||
"listMarker": "#BD93F999"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#1F1F2F",
|
||||
"link": "#1D7FC5",
|
||||
"linkHover": "#7C6BF5",
|
||||
"inlineCode": "#2FBF71",
|
||||
"inlineCodeBackground": "#F1F2ED",
|
||||
"inlineCode": "#007325",
|
||||
"inlineCodeBackground": "#EBEBE5",
|
||||
"blockquote": "#F7A14D",
|
||||
"blockquoteBorder": "#E2E3DA",
|
||||
"listMarker": "#7C6BF599"
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
"link": "#5a6d7a",
|
||||
"linkHover": "#93a56b",
|
||||
"inlineCode": "#93a56b",
|
||||
"inlineCodeBackground": "#23201c",
|
||||
"inlineCodeBackground": "#282522",
|
||||
"blockquote": "#a89888",
|
||||
"blockquoteBorder": "#f0e6d830",
|
||||
"listMarker": "#c47a3a99"
|
||||
|
||||
@@ -136,8 +136,8 @@
|
||||
"heading4": "#1a1612",
|
||||
"link": "#3d4f5a",
|
||||
"linkHover": "#4a6030",
|
||||
"inlineCode": "#4a6030",
|
||||
"inlineCodeBackground": "#ece5d6",
|
||||
"inlineCode": "#4A6030",
|
||||
"inlineCodeBackground": "#ECE8DE",
|
||||
"blockquote": "#5a5048",
|
||||
"blockquoteBorder": "#1a161230",
|
||||
"listMarker": "#8c552099"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"link": "#4385BE",
|
||||
"linkHover": "#205EA6",
|
||||
"inlineCode": "#A0AF53",
|
||||
"inlineCodeBackground": "#1C1B1A",
|
||||
"inlineCodeBackground": "#242222",
|
||||
"blockquote": "#878580",
|
||||
"blockquoteBorder": "#343331",
|
||||
"listMarker": "#D0A21599"
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
"heading4": "#100F0F",
|
||||
"link": "#205EA6",
|
||||
"linkHover": "#4385BE",
|
||||
"inlineCode": "#24837B",
|
||||
"inlineCodeBackground": "#f6f5ee",
|
||||
"inlineCode": "#0A6961",
|
||||
"inlineCodeBackground": "#F2F0E7",
|
||||
"blockquote": "#6F6E69",
|
||||
"blockquoteBorder": "#DAD8CE",
|
||||
"listMarker": "#AD830199"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#8EC07C",
|
||||
"linkHover": "#83A598",
|
||||
"inlineCode": "#B8BB26",
|
||||
"inlineCodeBackground": "#32302F",
|
||||
"inlineCodeBackground": "#353535",
|
||||
"blockquote": "#928374",
|
||||
"blockquoteBorder": "#504945",
|
||||
"listMarker": "#83A59899"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#3C3836",
|
||||
"link": "#427B58",
|
||||
"linkHover": "#076678",
|
||||
"inlineCode": "#79740E",
|
||||
"inlineCodeBackground": "#F2E5BC",
|
||||
"inlineCode": "#5F5A00",
|
||||
"inlineCodeBackground": "#EBE4C8",
|
||||
"blockquote": "#928374",
|
||||
"blockquoteBorder": "#D5C4A1",
|
||||
"listMarker": "#07667899"
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"link": "#56A8F5",
|
||||
"linkHover": "#6796f5",
|
||||
"inlineCode": "#6AAB73",
|
||||
"inlineCodeBackground": "#26282B",
|
||||
"inlineCodeBackground": "#2B2C2F",
|
||||
"blockquote": "#7A7E85",
|
||||
"blockquoteBorder": "#393B41",
|
||||
"listMarker": "#B3AE6099"
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"link": "#006DCC",
|
||||
"linkHover": "#3573F0",
|
||||
"inlineCode": "#067D17",
|
||||
"inlineCodeBackground": "#F5F7F9",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#8C8C8C",
|
||||
"blockquoteBorder": "#C9CCD6",
|
||||
"listMarker": "#9E880D99"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"link": "#7FB4CA",
|
||||
"linkHover": "#7E9CD8",
|
||||
"inlineCode": "#98BB6C",
|
||||
"inlineCodeBackground": "#16161D",
|
||||
"inlineCodeBackground": "#2C2C35",
|
||||
"blockquote": "#54546D",
|
||||
"blockquoteBorder": "#363646",
|
||||
"listMarker": "#FF9E3B99"
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
"heading4": "#545464",
|
||||
"link": "#5D57A3",
|
||||
"linkHover": "#4D699B",
|
||||
"inlineCode": "#6F894E",
|
||||
"inlineCodeBackground": "#e7e2c7",
|
||||
"inlineCode": "#496328",
|
||||
"inlineCodeBackground": "#E9E6C9",
|
||||
"blockquote": "#716E61",
|
||||
"blockquoteBorder": "#716E61",
|
||||
"listMarker": "#836F4A99"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"link": "#CCCCCC",
|
||||
"linkHover": "#FFFFFF",
|
||||
"inlineCode": "#B3B3B3",
|
||||
"inlineCodeBackground": "#1A1A1A",
|
||||
"inlineCodeBackground": "#0D0D0D",
|
||||
"blockquote": "#808080",
|
||||
"blockquoteBorder": "#333333",
|
||||
"listMarker": "#99999999"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"link": "#333333",
|
||||
"linkHover": "#000000",
|
||||
"inlineCode": "#4D4D4D",
|
||||
"inlineCodeBackground": "#f1f1f1",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#808080",
|
||||
"blockquoteBorder": "#D9D9D9",
|
||||
"listMarker": "#66666699"
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"link": "#CCCCCC",
|
||||
"linkHover": "#a2bee8",
|
||||
"inlineCode": "#a2bee8",
|
||||
"inlineCodeBackground": "#1A1A1A",
|
||||
"inlineCodeBackground": "#0D0D0D",
|
||||
"blockquote": "#808080",
|
||||
"blockquoteBorder": "#333333",
|
||||
"listMarker": "#99999999"
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
"heading4": "#262626",
|
||||
"link": "#333333",
|
||||
"linkHover": "#4a6a9e",
|
||||
"inlineCode": "#4a6a9e",
|
||||
"inlineCodeBackground": "#f1f1f1",
|
||||
"inlineCode": "#4A6A9E",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#808080",
|
||||
"blockquoteBorder": "#D9D9D9",
|
||||
"listMarker": "#66666699"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#66D9EF",
|
||||
"linkHover": "#AE81FF",
|
||||
"inlineCode": "#A6E22E",
|
||||
"inlineCodeBackground": "#27281F",
|
||||
"inlineCodeBackground": "#30312B",
|
||||
"blockquote": "#FD971F",
|
||||
"blockquoteBorder": "#343528",
|
||||
"listMarker": "#AE81FF99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#292318",
|
||||
"link": "#2D9AD7",
|
||||
"linkHover": "#BF7BFF",
|
||||
"inlineCode": "#4FB54B",
|
||||
"inlineCodeBackground": "#F8F2E6",
|
||||
"inlineCode": "#0F750B",
|
||||
"inlineCodeBackground": "#F0EBDF",
|
||||
"blockquote": "#F1A948",
|
||||
"blockquoteBorder": "#E9E0CF",
|
||||
"listMarker": "#BF7BFF99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#7FDBCA",
|
||||
"linkHover": "#82AAFF",
|
||||
"inlineCode": "#C5E478",
|
||||
"inlineCodeBackground": "#0B253A",
|
||||
"inlineCodeBackground": "#0E2334",
|
||||
"blockquote": "#5F7E97",
|
||||
"blockquoteBorder": "#1D3B53",
|
||||
"listMarker": "#82AAFF99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#403F53",
|
||||
"link": "#2AA298",
|
||||
"linkHover": "#4876D6",
|
||||
"inlineCode": "#2AA298",
|
||||
"inlineCodeBackground": "#F0F0F0",
|
||||
"inlineCode": "#00746A",
|
||||
"inlineCodeBackground": "#EEEEEE",
|
||||
"blockquote": "#7A8181",
|
||||
"blockquoteBorder": "#D9D9D9",
|
||||
"listMarker": "#4876D699"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#81A1C1",
|
||||
"linkHover": "#88C0D0",
|
||||
"inlineCode": "#A3BE8C",
|
||||
"inlineCodeBackground": "#252c3c",
|
||||
"inlineCodeBackground": "#2C313D",
|
||||
"blockquote": "#D08770",
|
||||
"blockquoteBorder": "#343A47",
|
||||
"listMarker": "#88C0D099"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#2E3440",
|
||||
"link": "#81A1C1",
|
||||
"linkHover": "#5E81AC",
|
||||
"inlineCode": "#4f7034",
|
||||
"inlineCodeBackground": "#E4E8F0",
|
||||
"inlineCode": "#35561A",
|
||||
"inlineCodeBackground": "#DFE2E7",
|
||||
"blockquote": "#D08770",
|
||||
"blockquoteBorder": "#D5DBE7",
|
||||
"listMarker": "#5E81AC99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#56B6C2",
|
||||
"linkHover": "#61AFEF",
|
||||
"inlineCode": "#a0c288",
|
||||
"inlineCodeBackground": "#232937",
|
||||
"inlineCodeBackground": "#2B2F37",
|
||||
"blockquote": "#E5C07B",
|
||||
"blockquoteBorder": "#323848",
|
||||
"listMarker": "#61AFEF99"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#2B303B",
|
||||
"link": "#61AFEF",
|
||||
"linkHover": "#528BFF",
|
||||
"inlineCode": "#327d4c",
|
||||
"inlineCodeBackground": "#EEF0F4",
|
||||
"inlineCode": "#186332",
|
||||
"inlineCodeBackground": "#E8E9EB",
|
||||
"blockquote": "#D19A66",
|
||||
"blockquoteBorder": "#DEE2EB",
|
||||
"listMarker": "#528BFF99"
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"link": "#5d99a9",
|
||||
"linkHover": "#6ba7b8",
|
||||
"inlineCode": "#76ad4f",
|
||||
"inlineCodeBackground": "#211f1d",
|
||||
"inlineCodeBackground": "#1F1C1B",
|
||||
"blockquote": "#8f8b81",
|
||||
"blockquoteBorder": "#302e2b",
|
||||
"listMarker": "#4d934e99"
|
||||
|
||||
@@ -137,8 +137,8 @@
|
||||
"heading4": "#393a34",
|
||||
"link": "#2e808f",
|
||||
"linkHover": "#296aa3",
|
||||
"inlineCode": "#1a8446",
|
||||
"inlineCodeBackground": "#f4f3f1",
|
||||
"inlineCode": "#006A2C",
|
||||
"inlineCodeBackground": "#F0EFED",
|
||||
"blockquote": "#6b6b63",
|
||||
"blockquoteBorder": "#d8d5d0",
|
||||
"listMarker": "#1e754f99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#2AA198",
|
||||
"linkHover": "#6C71C4",
|
||||
"inlineCode": "#859900",
|
||||
"inlineCodeBackground": "#022733",
|
||||
"inlineCodeBackground": "#0D2B32",
|
||||
"blockquote": "#B58900",
|
||||
"blockquoteBorder": "#20373F",
|
||||
"listMarker": "#6C71C499"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#586E75",
|
||||
"link": "#2AA198",
|
||||
"linkHover": "#268BD2",
|
||||
"inlineCode": "#859900",
|
||||
"inlineCodeBackground": "#F6EFDA",
|
||||
"inlineCode": "#576B00",
|
||||
"inlineCodeBackground": "#F0E9D6",
|
||||
"blockquote": "#B58900",
|
||||
"blockquoteBorder": "#E3E0CD",
|
||||
"listMarker": "#268BD299"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#7DCFFF",
|
||||
"linkHover": "#7AA2F7",
|
||||
"inlineCode": "#9ECE6A",
|
||||
"inlineCodeBackground": "#111428",
|
||||
"inlineCodeBackground": "#1C1E27",
|
||||
"blockquote": "#E0AF68",
|
||||
"blockquoteBorder": "#25283B",
|
||||
"listMarker": "#7AA2F799"
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"heading4": "#273153",
|
||||
"link": "#007197",
|
||||
"linkHover": "#2E7DE9",
|
||||
"inlineCode": "#587539",
|
||||
"inlineCodeBackground": "#DEE0EA",
|
||||
"inlineCode": "#3E5B1F",
|
||||
"inlineCodeBackground": "#D4D5DA",
|
||||
"blockquote": "#8C6C3E",
|
||||
"blockquoteBorder": "#CDD0DC",
|
||||
"listMarker": "#2E7DE999"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#A0A0A0",
|
||||
"linkHover": "#FFC799",
|
||||
"inlineCode": "#bba2a2",
|
||||
"inlineCodeBackground": "#292727",
|
||||
"inlineCodeBackground": "#222222",
|
||||
"blockquote": "#FFFFFF",
|
||||
"blockquoteBorder": "#1C1C1C",
|
||||
"listMarker": "#FFFFFF99"
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"link": "#717070",
|
||||
"linkHover": "#c48959",
|
||||
"inlineCode": "#665050",
|
||||
"inlineCodeBackground": "#f3f1f1",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#101010",
|
||||
"blockquoteBorder": "#E8E8E8",
|
||||
"listMarker": "#10101099"
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"link": "#4d9375",
|
||||
"linkHover": "#4d9375",
|
||||
"inlineCode": "#80a665",
|
||||
"inlineCodeBackground": "#121212",
|
||||
"inlineCodeBackground": "#1F1F1F",
|
||||
"blockquote": "#dedcd550",
|
||||
"blockquoteBorder": "#ffffff15",
|
||||
"listMarker": "#4d937599"
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
"heading4": "#2c2c28",
|
||||
"link": "#1e754f",
|
||||
"linkHover": "#1c6b48",
|
||||
"inlineCode": "#3a631e",
|
||||
"inlineCodeBackground": "#f7f3f395",
|
||||
"inlineCode": "#3A631E",
|
||||
"inlineCodeBackground": "#F2F2F2",
|
||||
"blockquote": "#2c2c2850",
|
||||
"blockquoteBorder": "#00000015",
|
||||
"listMarker": "#1c6b4899"
|
||||
|
||||
Reference in New Issue
Block a user