Merge remote main into local
This commit is contained in:
commit
f2841bdd74
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -477,6 +477,9 @@ jobs:
|
|||||||
eas-version: latest
|
eas-version: latest
|
||||||
packager: pnpm
|
packager: pnpm
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
# Its default caches go to the GitHub cache service; keep them on the runner.
|
||||||
|
eas-cache: false
|
||||||
|
expo-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
createMediaRuntime,
|
createMediaRuntime,
|
||||||
createMessagingContextLoader,
|
createMessagingContextLoader,
|
||||||
createMessagingTeamChatSender,
|
createMessagingTeamChatSender,
|
||||||
|
createPostgresReconciliationLeadership,
|
||||||
createProfileConsolidationScheduler,
|
createProfileConsolidationScheduler,
|
||||||
createRunExecutor,
|
createRunExecutor,
|
||||||
createRunSandbox,
|
createRunSandbox,
|
||||||
@ -360,7 +361,7 @@ export async function createApp(
|
|||||||
"https://172.*:*",
|
"https://172.*:*",
|
||||||
"http://*.local:*",
|
"http://*.local:*",
|
||||||
"https://*.local:*",
|
"https://*.local:*",
|
||||||
...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []),
|
...(env.extraTrustedOrigins ?? []),
|
||||||
],
|
],
|
||||||
beforeDeleteUser: async (userId) => {
|
beforeDeleteUser: async (userId) => {
|
||||||
const bots = await prisma.bot.findMany({
|
const bots = await prisma.bot.findMany({
|
||||||
@ -474,19 +475,23 @@ export async function createApp(
|
|||||||
cloudAgent,
|
cloudAgent,
|
||||||
media,
|
media,
|
||||||
});
|
});
|
||||||
|
let graphileWorkerHost: GraphileJobWorkerHost | undefined;
|
||||||
if (inMemoryJobs) {
|
if (inMemoryJobs) {
|
||||||
await inMemoryJobs.start(jobHandlers);
|
await inMemoryJobs.start(jobHandlers);
|
||||||
} else if (jobPool) {
|
} else if (jobPool) {
|
||||||
const graphileWorkerHost = new GraphileJobWorkerHost(jobPool, {
|
graphileWorkerHost = new GraphileJobWorkerHost(jobPool, {
|
||||||
concurrency: 4,
|
concurrency: parsePositiveInteger(process.env.GRAPHILE_WORKER_CONCURRENCY, 4),
|
||||||
pollInterval: 500,
|
pollInterval: 500,
|
||||||
});
|
});
|
||||||
await graphileWorkerHost.start(jobHandlers);
|
await graphileWorkerHost.start(jobHandlers);
|
||||||
}
|
}
|
||||||
const reconciler = inMemoryJobs
|
const reconciler =
|
||||||
|
inMemoryJobs || !prismaOverride
|
||||||
? createJobReconciler({
|
? createJobReconciler({
|
||||||
prisma,
|
prisma,
|
||||||
jobs,
|
jobs,
|
||||||
|
events,
|
||||||
|
leadership: jobPool ? createPostgresReconciliationLeadership(jobPool) : undefined,
|
||||||
reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }),
|
reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }),
|
||||||
reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }),
|
reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }),
|
||||||
reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }),
|
reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }),
|
||||||
@ -612,7 +617,7 @@ export async function createApp(
|
|||||||
actor,
|
actor,
|
||||||
signal: c.req.raw.signal,
|
signal: c.req.raw.signal,
|
||||||
userAgent: c.req.header("user-agent"),
|
userAgent: c.req.header("user-agent"),
|
||||||
webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin),
|
webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin, env.extraTrustedOrigins),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (matched) return c.newResponse(response.body, response);
|
if (matched) return c.newResponse(response.body, response);
|
||||||
@ -1006,6 +1011,7 @@ export async function createApp(
|
|||||||
await teamChatBridge?.stop();
|
await teamChatBridge?.stop();
|
||||||
await email?.drain?.();
|
await email?.drain?.();
|
||||||
await reconciler?.stop();
|
await reconciler?.stop();
|
||||||
|
await graphileWorkerHost?.stop();
|
||||||
await jobs.close();
|
await jobs.close();
|
||||||
await realtime.close();
|
await realtime.close();
|
||||||
await connector.stop();
|
await connector.stop();
|
||||||
@ -1021,6 +1027,7 @@ export async function createApp(
|
|||||||
function isTrustedOrigin(origin: string, env: AppEnv) {
|
function isTrustedOrigin(origin: string, env: AppEnv) {
|
||||||
if (!origin) return true;
|
if (!origin) return true;
|
||||||
if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true;
|
if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true;
|
||||||
|
if (env.extraTrustedOrigins?.includes(origin)) return true;
|
||||||
if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true;
|
if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true;
|
||||||
try {
|
try {
|
||||||
const host = new URL(origin).hostname;
|
const host = new URL(origin).hostname;
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export interface AppEnv {
|
|||||||
authSecret: string;
|
authSecret: string;
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
webOrigin: string;
|
webOrigin: string;
|
||||||
|
extraTrustedOrigins: string[];
|
||||||
privacyPolicyUrl?: string;
|
privacyPolicyUrl?: string;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
apiHost: string;
|
apiHost: string;
|
||||||
@ -109,6 +110,10 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
|
|||||||
authSecret,
|
authSecret,
|
||||||
authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
||||||
webOrigin: source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
webOrigin: source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
||||||
|
extraTrustedOrigins: (source.EXTRA_TRUSTED_ORIGINS ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL),
|
privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL),
|
||||||
apiUrl: source.API_URL ?? "http://127.0.0.1:3100",
|
apiUrl: source.API_URL ?? "http://127.0.0.1:3100",
|
||||||
apiHost: source.API_HOST ?? "127.0.0.1",
|
apiHost: source.API_HOST ?? "127.0.0.1",
|
||||||
|
|||||||
@ -135,6 +135,7 @@ import {
|
|||||||
createProjectGoalRepos,
|
createProjectGoalRepos,
|
||||||
createRepos,
|
createRepos,
|
||||||
createSpaceForMember,
|
createSpaceForMember,
|
||||||
|
DuplicateSpaceNameError,
|
||||||
deleteEmptySpaceForMember,
|
deleteEmptySpaceForMember,
|
||||||
deleteUnreferencedCredentialSecret,
|
deleteUnreferencedCredentialSecret,
|
||||||
ensureMainConversation,
|
ensureMainConversation,
|
||||||
@ -270,7 +271,10 @@ import {
|
|||||||
followUpGroupThreadMessage,
|
followUpGroupThreadMessage,
|
||||||
groupDefaultBotId,
|
groupDefaultBotId,
|
||||||
reactToThreadMessage,
|
reactToThreadMessage,
|
||||||
|
removeQueuedMessage,
|
||||||
resolveThreadTarget,
|
resolveThreadTarget,
|
||||||
|
resumeThreadRuns,
|
||||||
|
sendQueuedThreadMessagesNow,
|
||||||
sendThreadMessage,
|
sendThreadMessage,
|
||||||
setThreadUnreadState,
|
setThreadUnreadState,
|
||||||
stopThreadRuns,
|
stopThreadRuns,
|
||||||
@ -783,6 +787,16 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
const goal =
|
const goal =
|
||||||
input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null);
|
input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null);
|
||||||
if (goal) {
|
if (goal) {
|
||||||
|
const existingGoal = await deps.prisma.memoryDocument.findFirst({
|
||||||
|
where: { spaceId: space.id, scope: "user", path: "PROJECT_GOAL.md" },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (existingGoal) {
|
||||||
|
await deps.prisma.memoryDocument.update({
|
||||||
|
where: { id: existingGoal.id },
|
||||||
|
data: { content: goal },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await deps.prisma.memoryDocument.create({
|
await deps.prisma.memoryDocument.create({
|
||||||
data: {
|
data: {
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
@ -793,6 +807,7 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// A project starts as a conversation with its own main agent.
|
// A project starts as a conversation with its own main agent.
|
||||||
await ensureMainConversation(deps.prisma, context.actor.userId, space.id);
|
await ensureMainConversation(deps.prisma, context.actor.userId, space.id);
|
||||||
const spaceActor = await requireMembership(deps.prisma, context.actor.userId, space.id);
|
const spaceActor = await requireMembership(deps.prisma, context.actor.userId, space.id);
|
||||||
@ -873,7 +888,7 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
throw new ORPCError("NOT_FOUND", { message: error.message });
|
throw new ORPCError("NOT_FOUND", { message: error.message });
|
||||||
}
|
}
|
||||||
if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN");
|
if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN");
|
||||||
if (error instanceof InvalidSpaceNameError) {
|
if (error instanceof InvalidSpaceNameError || error instanceof DuplicateSpaceNameError) {
|
||||||
throw new ORPCError("BAD_REQUEST", { message: error.message });
|
throw new ORPCError("BAD_REQUEST", { message: error.message });
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@ -2050,6 +2065,42 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
await stopThreadRuns(deps, context.actor, target);
|
await stopThreadRuns(deps, context.actor, target);
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}),
|
}),
|
||||||
|
resume: authed.threads.resume.handler(async ({ context, input }) => {
|
||||||
|
if ((await modelSetup(deps, context.actor)).needsModel) {
|
||||||
|
throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." });
|
||||||
|
}
|
||||||
|
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||||
|
if (target.kind === "bot") {
|
||||||
|
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||||
|
}
|
||||||
|
return resumeThreadRuns(deps, context.actor, target);
|
||||||
|
}),
|
||||||
|
sendNow: authed.threads.sendNow.handler(async ({ context, input }) => {
|
||||||
|
if ((await modelSetup(deps, context.actor)).needsModel) {
|
||||||
|
throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." });
|
||||||
|
}
|
||||||
|
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||||
|
if (target.kind === "bot") {
|
||||||
|
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||||
|
}
|
||||||
|
return sendQueuedThreadMessagesNow(deps, context.actor, target, {
|
||||||
|
messageId: input.messageId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
removeQueuedMessage: authed.threads.removeQueuedMessage.handler(
|
||||||
|
async ({ context, input }) => {
|
||||||
|
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||||
|
if (target.kind === "bot") {
|
||||||
|
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||||
|
}
|
||||||
|
return removeQueuedMessage(
|
||||||
|
{ prisma: deps.prisma, events: deps.events },
|
||||||
|
context.actor,
|
||||||
|
target,
|
||||||
|
input.messageId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
clear: authed.threads.clear.handler(async ({ context, input }) => {
|
clear: authed.threads.clear.handler(async ({ context, input }) => {
|
||||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||||
const contextBotId = target.kind === "bot" ? target.botId : groupDefaultBotId(target);
|
const contextBotId = target.kind === "bot" ? target.botId : groupDefaultBotId(target);
|
||||||
@ -5623,13 +5674,13 @@ async function spaceNavigationDto(
|
|||||||
// The user's own main agent of a project is part of the project, not content to clear.
|
// The user's own main agent of a project is part of the project, not content to clear.
|
||||||
deps.prisma.bot.findMany({
|
deps.prisma.bot.findMany({
|
||||||
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||||
select: { spaceId: true },
|
select: { spaceId: true, userId: true },
|
||||||
distinct: ["spaceId"],
|
distinct: ["spaceId", "userId"],
|
||||||
}),
|
}),
|
||||||
deps.prisma.chatGroup.findMany({
|
deps.prisma.chatGroup.findMany({
|
||||||
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||||
select: { spaceId: true },
|
select: { spaceId: true, userId: true },
|
||||||
distinct: ["spaceId"],
|
distinct: ["spaceId", "userId"],
|
||||||
}),
|
}),
|
||||||
deps.prisma.memoryDocument.findMany({
|
deps.prisma.memoryDocument.findMany({
|
||||||
where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" },
|
where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" },
|
||||||
@ -5640,6 +5691,11 @@ async function spaceNavigationDto(
|
|||||||
...contentBots.map((row) => row.spaceId),
|
...contentBots.map((row) => row.spaceId),
|
||||||
...contentGroups.map((row) => row.spaceId),
|
...contentGroups.map((row) => row.spaceId),
|
||||||
]);
|
]);
|
||||||
|
const spacesBlockedByOtherMember = new Set(
|
||||||
|
[...contentBots, ...contentGroups]
|
||||||
|
.filter((row) => row.userId !== actor.userId)
|
||||||
|
.map((row) => row.spaceId),
|
||||||
|
);
|
||||||
const goalBySpaceId = new Map(goalDocuments.map((doc) => [doc.spaceId, doc.content]));
|
const goalBySpaceId = new Map(goalDocuments.map((doc) => [doc.spaceId, doc.content]));
|
||||||
const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId);
|
const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId);
|
||||||
if (!currentMembership) throw new IsolationError();
|
if (!currentMembership) throw new IsolationError();
|
||||||
@ -5676,7 +5732,7 @@ async function spaceNavigationDto(
|
|||||||
membership.role === "owner" &&
|
membership.role === "owner" &&
|
||||||
!membership.space.isDefault &&
|
!membership.space.isDefault &&
|
||||||
memberships.length > 1 &&
|
memberships.length > 1 &&
|
||||||
!spacesWithContent.has(membership.spaceId) &&
|
!spacesBlockedByOtherMember.has(membership.spaceId) &&
|
||||||
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
|
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
|
||||||
canEdit: membership.role === "owner",
|
canEdit: membership.role === "owner",
|
||||||
// A linked folder is on the owner's host; other members see neither its path nor remote.
|
// A linked folder is on the owner's host; other members see neither its path nor remote.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts";
|
import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts";
|
||||||
import { extractLinksFromText, matchesSearchQuery, snippetAroundMatch } from "@rakazo/core";
|
import { extractLinksFromText, matchesSearchQuery, snippetAroundMatch } from "@rakazo/core";
|
||||||
import { Prisma, type PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
|
import { Prisma } from "@rakazo/db";
|
||||||
|
|
||||||
const SEARCH_LIMIT = 25;
|
const SEARCH_LIMIT = 25;
|
||||||
/** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */
|
/** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */
|
||||||
@ -64,7 +65,8 @@ export async function querySpaceSearch(
|
|||||||
bucket.push(hit);
|
bucket.push(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bots = await prisma.bot.findMany({
|
const [bots, groups, artifacts, groupArtifacts, routines] = await Promise.all([
|
||||||
|
prisma.bot.findMany({
|
||||||
where: {
|
where: {
|
||||||
spaceId: actor.spaceId,
|
spaceId: actor.spaceId,
|
||||||
userId: actor.userId,
|
userId: actor.userId,
|
||||||
@ -76,7 +78,52 @@ export async function querySpaceSearch(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
take: CONVERSATION_HIT_LIMIT,
|
take: CONVERSATION_HIT_LIMIT,
|
||||||
});
|
}),
|
||||||
|
prisma.chatGroup.findMany({
|
||||||
|
where: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
userId: actor.userId,
|
||||||
|
name: { contains: query, mode: "insensitive" },
|
||||||
|
},
|
||||||
|
take: CONVERSATION_HIT_LIMIT,
|
||||||
|
}),
|
||||||
|
prisma.artifact.findMany({
|
||||||
|
where: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
userId: actor.userId,
|
||||||
|
groupId: null,
|
||||||
|
botId: { not: null },
|
||||||
|
name: { contains: query, mode: "insensitive" },
|
||||||
|
bot: { archivedAt: null },
|
||||||
|
},
|
||||||
|
include: { bot: { select: { name: true } } },
|
||||||
|
take: SEARCH_LIMIT,
|
||||||
|
}),
|
||||||
|
prisma.artifact.findMany({
|
||||||
|
where: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
userId: actor.userId,
|
||||||
|
groupId: { not: null },
|
||||||
|
name: { contains: query, mode: "insensitive" },
|
||||||
|
},
|
||||||
|
include: { group: { select: { name: true } } },
|
||||||
|
take: SEARCH_LIMIT,
|
||||||
|
}),
|
||||||
|
prisma.routine.findMany({
|
||||||
|
where: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
userId: actor.userId,
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ prompt: { contains: query, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
bot: { archivedAt: null },
|
||||||
|
},
|
||||||
|
include: { bot: { select: { name: true } } },
|
||||||
|
take: SEARCH_LIMIT,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
for (const bot of bots) {
|
for (const bot of bots) {
|
||||||
pushInto(
|
pushInto(
|
||||||
conversationHits,
|
conversationHits,
|
||||||
@ -91,14 +138,6 @@ export async function querySpaceSearch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = await prisma.chatGroup.findMany({
|
|
||||||
where: {
|
|
||||||
spaceId: actor.spaceId,
|
|
||||||
userId: actor.userId,
|
|
||||||
name: { contains: query, mode: "insensitive" },
|
|
||||||
},
|
|
||||||
take: CONVERSATION_HIT_LIMIT,
|
|
||||||
});
|
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
pushInto(
|
pushInto(
|
||||||
conversationHits,
|
conversationHits,
|
||||||
@ -114,27 +153,72 @@ export async function querySpaceSearch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length);
|
const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length);
|
||||||
|
const pattern = `%${query}%`;
|
||||||
|
|
||||||
const artifacts = await prisma.artifact.findMany({
|
const [botArtifactMessages, groupArtifactMessages, messageRows, groupMessageRows] =
|
||||||
where: {
|
await Promise.all([
|
||||||
spaceId: actor.spaceId,
|
findArtifactMessages(
|
||||||
userId: actor.userId,
|
|
||||||
groupId: null,
|
|
||||||
botId: { not: null },
|
|
||||||
name: { contains: query, mode: "insensitive" },
|
|
||||||
bot: { archivedAt: null },
|
|
||||||
},
|
|
||||||
include: { bot: { select: { name: true } } },
|
|
||||||
take: SEARCH_LIMIT,
|
|
||||||
});
|
|
||||||
const botArtifactMessages = await findArtifactMessages(
|
|
||||||
prisma,
|
prisma,
|
||||||
actor,
|
actor,
|
||||||
artifacts.flatMap((artifact) =>
|
artifacts.flatMap((artifact) =>
|
||||||
artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [],
|
artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [],
|
||||||
),
|
),
|
||||||
"bot",
|
"bot",
|
||||||
);
|
),
|
||||||
|
findArtifactMessages(
|
||||||
|
prisma,
|
||||||
|
actor,
|
||||||
|
groupArtifacts.flatMap((artifact) =>
|
||||||
|
artifact.groupId && artifact.group
|
||||||
|
? [{ id: artifact.id, targetId: artifact.groupId }]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
"group",
|
||||||
|
),
|
||||||
|
prisma.$queryRaw<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
threadId: string;
|
||||||
|
seq: number;
|
||||||
|
blocks: Prisma.JsonValue;
|
||||||
|
botId: string;
|
||||||
|
botName: string;
|
||||||
|
}>
|
||||||
|
>`
|
||||||
|
SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName"
|
||||||
|
FROM messages m
|
||||||
|
INNER JOIN threads t ON t.id = m."threadId"
|
||||||
|
INNER JOIN bots b ON b.id = t."botId"
|
||||||
|
WHERE t."spaceId" = ${actor.spaceId}
|
||||||
|
AND t."userId" = ${actor.userId}
|
||||||
|
AND b."archivedAt" IS NULL
|
||||||
|
AND m.blocks::text ILIKE ${pattern}
|
||||||
|
ORDER BY m."createdAt" DESC
|
||||||
|
LIMIT ${SEARCH_LIMIT}
|
||||||
|
`,
|
||||||
|
prisma.$queryRaw<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
threadId: string;
|
||||||
|
seq: number;
|
||||||
|
blocks: Prisma.JsonValue;
|
||||||
|
groupId: string;
|
||||||
|
groupName: string;
|
||||||
|
}>
|
||||||
|
>`
|
||||||
|
SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName"
|
||||||
|
FROM messages m
|
||||||
|
INNER JOIN threads t ON t.id = m."threadId"
|
||||||
|
INNER JOIN chat_groups g ON g.id = t."groupId"
|
||||||
|
WHERE t."spaceId" = ${actor.spaceId}
|
||||||
|
AND t."userId" = ${actor.userId}
|
||||||
|
AND t."groupId" IS NOT NULL
|
||||||
|
AND m.blocks::text ILIKE ${pattern}
|
||||||
|
ORDER BY m."createdAt" DESC
|
||||||
|
LIMIT ${SEARCH_LIMIT}
|
||||||
|
`,
|
||||||
|
]);
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
if (!artifact.botId || !artifact.bot) continue;
|
if (!artifact.botId || !artifact.bot) continue;
|
||||||
const message = botArtifactMessages.get(artifact.id);
|
const message = botArtifactMessages.get(artifact.id);
|
||||||
@ -155,24 +239,6 @@ export async function querySpaceSearch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupArtifacts = await prisma.artifact.findMany({
|
|
||||||
where: {
|
|
||||||
spaceId: actor.spaceId,
|
|
||||||
userId: actor.userId,
|
|
||||||
groupId: { not: null },
|
|
||||||
name: { contains: query, mode: "insensitive" },
|
|
||||||
},
|
|
||||||
include: { group: { select: { name: true } } },
|
|
||||||
take: SEARCH_LIMIT,
|
|
||||||
});
|
|
||||||
const groupArtifactMessages = await findArtifactMessages(
|
|
||||||
prisma,
|
|
||||||
actor,
|
|
||||||
groupArtifacts.flatMap((artifact) =>
|
|
||||||
artifact.groupId && artifact.group ? [{ id: artifact.id, targetId: artifact.groupId }] : [],
|
|
||||||
),
|
|
||||||
"group",
|
|
||||||
);
|
|
||||||
for (const artifact of groupArtifacts) {
|
for (const artifact of groupArtifacts) {
|
||||||
if (!artifact.groupId || !artifact.group) continue;
|
if (!artifact.groupId || !artifact.group) continue;
|
||||||
const message = groupArtifactMessages.get(artifact.id);
|
const message = groupArtifactMessages.get(artifact.id);
|
||||||
@ -193,19 +259,6 @@ export async function querySpaceSearch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const routines = await prisma.routine.findMany({
|
|
||||||
where: {
|
|
||||||
spaceId: actor.spaceId,
|
|
||||||
userId: actor.userId,
|
|
||||||
OR: [
|
|
||||||
{ name: { contains: query, mode: "insensitive" } },
|
|
||||||
{ prompt: { contains: query, mode: "insensitive" } },
|
|
||||||
],
|
|
||||||
bot: { archivedAt: null },
|
|
||||||
},
|
|
||||||
include: { bot: { select: { name: true } } },
|
|
||||||
take: SEARCH_LIMIT,
|
|
||||||
});
|
|
||||||
for (const routine of routines) {
|
for (const routine of routines) {
|
||||||
pushInto(
|
pushInto(
|
||||||
contentHits,
|
contentHits,
|
||||||
@ -221,29 +274,6 @@ export async function querySpaceSearch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pattern = `%${query}%`;
|
|
||||||
const messageRows = await prisma.$queryRaw<
|
|
||||||
Array<{
|
|
||||||
id: string;
|
|
||||||
threadId: string;
|
|
||||||
seq: number;
|
|
||||||
blocks: Prisma.JsonValue;
|
|
||||||
botId: string;
|
|
||||||
botName: string;
|
|
||||||
}>
|
|
||||||
>`
|
|
||||||
SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName"
|
|
||||||
FROM messages m
|
|
||||||
INNER JOIN threads t ON t.id = m."threadId"
|
|
||||||
INNER JOIN bots b ON b.id = t."botId"
|
|
||||||
WHERE t."spaceId" = ${actor.spaceId}
|
|
||||||
AND t."userId" = ${actor.userId}
|
|
||||||
AND b."archivedAt" IS NULL
|
|
||||||
AND m.blocks::text ILIKE ${pattern}
|
|
||||||
ORDER BY m."createdAt" DESC
|
|
||||||
LIMIT ${SEARCH_LIMIT}
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const row of messageRows) {
|
for (const row of messageRows) {
|
||||||
pushMessageHits(row.blocks as MessageBlock[], {
|
pushMessageHits(row.blocks as MessageBlock[], {
|
||||||
botId: row.botId,
|
botId: row.botId,
|
||||||
@ -255,28 +285,6 @@ export async function querySpaceSearch(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupMessageRows = await prisma.$queryRaw<
|
|
||||||
Array<{
|
|
||||||
id: string;
|
|
||||||
threadId: string;
|
|
||||||
seq: number;
|
|
||||||
blocks: Prisma.JsonValue;
|
|
||||||
groupId: string;
|
|
||||||
groupName: string;
|
|
||||||
}>
|
|
||||||
>`
|
|
||||||
SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName"
|
|
||||||
FROM messages m
|
|
||||||
INNER JOIN threads t ON t.id = m."threadId"
|
|
||||||
INNER JOIN chat_groups g ON g.id = t."groupId"
|
|
||||||
WHERE t."spaceId" = ${actor.spaceId}
|
|
||||||
AND t."userId" = ${actor.userId}
|
|
||||||
AND t."groupId" IS NOT NULL
|
|
||||||
AND m.blocks::text ILIKE ${pattern}
|
|
||||||
ORDER BY m."createdAt" DESC
|
|
||||||
LIMIT ${SEARCH_LIMIT}
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const row of groupMessageRows) {
|
for (const row of groupMessageRows) {
|
||||||
pushMessageHits(row.blocks as MessageBlock[], {
|
pushMessageHits(row.blocks as MessageBlock[], {
|
||||||
groupId: row.groupId,
|
groupId: row.groupId,
|
||||||
|
|||||||
@ -88,4 +88,77 @@ describe("model team chat engagement judge", () => {
|
|||||||
expect(instructions).toContain("(Europe/Berlin, the user's time zone)");
|
expect(instructions).toContain("(Europe/Berlin, the user's time zone)");
|
||||||
expect(instructions).toContain("low-cost engagement judge");
|
expect(instructions).toContain("low-cost engagement judge");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers the space fast model tier when mapped and no explicit judge override is set", async () => {
|
||||||
|
let selectedModel: { provider: string; id: string; apiKey?: string } | undefined;
|
||||||
|
const runtime = {
|
||||||
|
run: vi.fn((request: { model: { provider: string; id: string; apiKey?: string } }) => {
|
||||||
|
selectedModel = request.model;
|
||||||
|
return (async function* () {
|
||||||
|
yield { type: "done" as const, text: '{"act":true,"reason":"Direct mention."}' };
|
||||||
|
})();
|
||||||
|
}),
|
||||||
|
} as unknown as AgentRuntime;
|
||||||
|
const prisma = {
|
||||||
|
deploymentSettings: { findUnique: vi.fn(async () => null) },
|
||||||
|
spaceModelPreference: { findFirst: vi.fn(async () => null) },
|
||||||
|
spaceModelTier: {
|
||||||
|
findUnique: vi.fn(async () => ({
|
||||||
|
tier: "fast",
|
||||||
|
credentialId: "cred-fast",
|
||||||
|
modelId: "gemma4:latest",
|
||||||
|
credential: {
|
||||||
|
id: "cred-fast",
|
||||||
|
userId: "user-1",
|
||||||
|
provider: "openai-compatible",
|
||||||
|
secretId: "sec-fast",
|
||||||
|
preferences: [{ isDefault: false, modelId: null }],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
findFirst: vi.fn(async () => ({ id: "sec-fast", ciphertext: "cipher-fast" })),
|
||||||
|
},
|
||||||
|
userProfile: { findUnique: vi.fn(async () => ({ timeZone: "UTC" })) },
|
||||||
|
} as unknown as PrismaClient;
|
||||||
|
const secrets = {
|
||||||
|
load: vi.fn(() =>
|
||||||
|
JSON.stringify({
|
||||||
|
kind: "openai_compatible",
|
||||||
|
apiKey: "local-key",
|
||||||
|
baseUrl: "http://127.0.0.1:11434/v1",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
} as unknown as EncryptedSecretStore;
|
||||||
|
|
||||||
|
const judge = new ModelTeamChatEngagementJudge({
|
||||||
|
prisma,
|
||||||
|
runtime,
|
||||||
|
secrets,
|
||||||
|
deploymentProvider: "openrouter",
|
||||||
|
deploymentModel: "expensive-default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const decision = await judge.decide({
|
||||||
|
bot: {
|
||||||
|
id: "bot-1",
|
||||||
|
spaceId: "space-1",
|
||||||
|
userId: "user-1",
|
||||||
|
name: "Arthur",
|
||||||
|
modelProvider: "anthropic",
|
||||||
|
modelId: "claude-opus",
|
||||||
|
},
|
||||||
|
channelId: "C1",
|
||||||
|
rules: "",
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision).toEqual({ act: true, reason: "Direct mention.", askedByEventId: undefined });
|
||||||
|
expect(selectedModel).toMatchObject({
|
||||||
|
provider: "openai-compatible",
|
||||||
|
id: "gemma4:latest",
|
||||||
|
apiKey: "local-key",
|
||||||
|
baseUrl: "http://127.0.0.1:11434/v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit";
|
import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit";
|
||||||
|
import type { EncryptedSecretStore } from "@rakazo/adapters";
|
||||||
import {
|
import {
|
||||||
type EncryptedSecretStore,
|
|
||||||
formatCurrentTimeInstruction,
|
formatCurrentTimeInstruction,
|
||||||
loadUserTimeZone,
|
loadUserTimeZone,
|
||||||
resolveModelAuth,
|
resolveModelAuth,
|
||||||
serializeModelSecret,
|
serializeModelSecret,
|
||||||
toOAuthCredential,
|
toOAuthCredential,
|
||||||
} from "@rakazo/adapters";
|
} from "@rakazo/adapters";
|
||||||
import { findDefaultModelCredential, findModelCredential, type PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
|
import { findDefaultModelCredential, findModelCredential, findSpaceModelTier } from "@rakazo/db";
|
||||||
import { getLogger } from "@rakazo/logging";
|
import { getLogger } from "@rakazo/logging";
|
||||||
|
|
||||||
const MAX_RULES_CHARS = 4_000;
|
const MAX_RULES_CHARS = 4_000;
|
||||||
@ -191,27 +192,39 @@ export class ModelTeamChatEngagementJudge implements TeamChatEngagementJudge {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
} | null> {
|
} | null> {
|
||||||
const settings = await this.deps.prisma.deploymentSettings.findUnique({
|
|
||||||
where: { id: "default" },
|
|
||||||
});
|
|
||||||
const scope = { userId: bot.userId, spaceId: bot.spaceId };
|
const scope = { userId: bot.userId, spaceId: bot.spaceId };
|
||||||
const defaultCredential = await findDefaultModelCredential(this.deps.prisma, scope);
|
const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride);
|
||||||
|
const fastTier =
|
||||||
|
!explicitJudgeOverride && this.deps.prisma.spaceModelTier
|
||||||
|
? await findSpaceModelTier(this.deps.prisma, scope, "fast")
|
||||||
|
: null;
|
||||||
|
const defaultCredential = fastTier
|
||||||
|
? null
|
||||||
|
: await findDefaultModelCredential(this.deps.prisma, scope);
|
||||||
const requestedProvider = this.deps.providerOverride ?? bot.modelProvider;
|
const requestedProvider = this.deps.providerOverride ?? bot.modelProvider;
|
||||||
const requestedModel = this.deps.modelOverride ?? bot.modelId;
|
const requestedModel = this.deps.modelOverride ?? bot.modelId;
|
||||||
const overrideCredential = requestedProvider
|
const overrideCredential =
|
||||||
|
!fastTier && requestedProvider
|
||||||
? await findModelCredential(this.deps.prisma, scope, requestedProvider)
|
? await findModelCredential(this.deps.prisma, scope, requestedProvider)
|
||||||
: null;
|
: null;
|
||||||
const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride);
|
|
||||||
const useOverride = Boolean(
|
const useOverride = Boolean(
|
||||||
requestedProvider && requestedModel && (explicitJudgeOverride || overrideCredential),
|
requestedProvider && requestedModel && (explicitJudgeOverride || overrideCredential),
|
||||||
);
|
);
|
||||||
const credential = useOverride ? overrideCredential : defaultCredential;
|
const credential =
|
||||||
|
fastTier?.credential ?? (useOverride ? overrideCredential : defaultCredential);
|
||||||
|
const settings = fastTier
|
||||||
|
? null
|
||||||
|
: await this.deps.prisma.deploymentSettings.findUnique({
|
||||||
|
where: { id: "default" },
|
||||||
|
});
|
||||||
const provider =
|
const provider =
|
||||||
|
fastTier?.credential.provider ??
|
||||||
(useOverride ? requestedProvider : null) ??
|
(useOverride ? requestedProvider : null) ??
|
||||||
credential?.provider ??
|
credential?.provider ??
|
||||||
settings?.defaultModelProvider ??
|
settings?.defaultModelProvider ??
|
||||||
this.deps.deploymentProvider;
|
this.deps.deploymentProvider;
|
||||||
const modelId =
|
const modelId =
|
||||||
|
fastTier?.modelId ??
|
||||||
(useOverride ? requestedModel : null) ??
|
(useOverride ? requestedModel : null) ??
|
||||||
credential?.defaultModel ??
|
credential?.defaultModel ??
|
||||||
settings?.defaultModelId ??
|
settings?.defaultModelId ??
|
||||||
|
|||||||
@ -2,15 +2,17 @@ import type { SandboxProvider } from "@rakazo/adapter-kit";
|
|||||||
import type { Actor } from "@rakazo/contracts";
|
import type { Actor } from "@rakazo/contracts";
|
||||||
import type { PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ThreadTarget } from "./thread-target.js";
|
||||||
import {
|
import {
|
||||||
cancelSupersededQueuedRuns,
|
cancelSupersededQueuedRuns,
|
||||||
followUpGroupThreadMessage,
|
followUpGroupThreadMessage,
|
||||||
groupDefaultBotId,
|
groupDefaultBotId,
|
||||||
reactToThreadMessage,
|
reactToThreadMessage,
|
||||||
resolveGroupFollowUpBotId,
|
resolveGroupFollowUpBotId,
|
||||||
|
resumeThreadRuns,
|
||||||
|
sendQueuedThreadMessagesNow,
|
||||||
sendThreadMessage,
|
sendThreadMessage,
|
||||||
stopThreadRuns,
|
stopThreadRuns,
|
||||||
type ThreadTarget,
|
|
||||||
threadHead,
|
threadHead,
|
||||||
threadSnapshot,
|
threadSnapshot,
|
||||||
} from "./thread-target.js";
|
} from "./thread-target.js";
|
||||||
@ -2118,4 +2120,192 @@ describe("stopThreadRuns", () => {
|
|||||||
expect.objectContaining({ runId: "run-b" }),
|
expect.objectContaining({ runId: "run-b" }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sendQueuedThreadMessagesNow cancels the active run, preserves pending steering, and enqueues an immediate continuation run", async () => {
|
||||||
|
const queuedCreatedAt = new Date("2026-01-01T10:00:05.000Z");
|
||||||
|
const transaction = {
|
||||||
|
$queryRaw: vi.fn(),
|
||||||
|
steeringMessage: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "steer-1",
|
||||||
|
botId: "bot-a",
|
||||||
|
userId: "user-1",
|
||||||
|
messageId: "msg-queued-1",
|
||||||
|
claimedAt: null,
|
||||||
|
message: {
|
||||||
|
id: "msg-queued-1",
|
||||||
|
blocks: [{ kind: "text", text: "Send this right now" }],
|
||||||
|
seq: 2,
|
||||||
|
createdAt: queuedCreatedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
|
},
|
||||||
|
run: {
|
||||||
|
updateManyAndReturn: vi.fn().mockResolvedValue([{ id: "run-old", taskId: "task-old" }]),
|
||||||
|
create: vi.fn().mockResolvedValue({ id: "run-new", status: "queued" }),
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
|
create: vi.fn().mockResolvedValue({ id: "task-new" }),
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
|
},
|
||||||
|
computerExecutionLease: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
computer: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
findFirst: vi.fn().mockResolvedValue({ seq: 42 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const prisma = {
|
||||||
|
$transaction: vi.fn(async (callback: (client: typeof transaction) => unknown) =>
|
||||||
|
callback(transaction),
|
||||||
|
),
|
||||||
|
computerExecutionLease: {
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||||
|
},
|
||||||
|
computer: {
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||||
|
},
|
||||||
|
} as unknown as PrismaClient;
|
||||||
|
const jobs = {
|
||||||
|
enqueue: vi.fn().mockResolvedValue(undefined),
|
||||||
|
cancel: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const events = {
|
||||||
|
notify: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const actor = { spaceId: "workspace-1", userId: "user-1" } as Actor;
|
||||||
|
const target = {
|
||||||
|
kind: "bot",
|
||||||
|
botId: "bot-a",
|
||||||
|
threadId: "thread-1",
|
||||||
|
bot: {} as never,
|
||||||
|
} satisfies ThreadTarget;
|
||||||
|
|
||||||
|
const result = await sendQueuedThreadMessagesNow(
|
||||||
|
{
|
||||||
|
prisma,
|
||||||
|
sandbox: {} as SandboxProvider,
|
||||||
|
events: events as never,
|
||||||
|
jobs: jobs as never,
|
||||||
|
},
|
||||||
|
actor,
|
||||||
|
target,
|
||||||
|
{ messageId: "msg-queued-1" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ ok: true, runIds: ["run-new"] });
|
||||||
|
expect(transaction.run.updateManyAndReturn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
threadId: "thread-1",
|
||||||
|
botId: { in: ["bot-a"] },
|
||||||
|
}),
|
||||||
|
data: expect.objectContaining({ status: "cancelled" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(transaction.steeringMessage.updateMany).toHaveBeenCalledWith({
|
||||||
|
where: { id: { in: ["steer-1"] } },
|
||||||
|
data: { runId: "run-new", claimedAt: null },
|
||||||
|
});
|
||||||
|
expect(jobs.cancel).toHaveBeenCalledWith("run:run-old");
|
||||||
|
expect(jobs.enqueue).toHaveBeenCalledWith({
|
||||||
|
name: "run.continue",
|
||||||
|
payload: { runId: "run-new" },
|
||||||
|
replaceKey: "run:run-new",
|
||||||
|
});
|
||||||
|
expect(events.notify).toHaveBeenCalledWith("thread-1", 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resumeThreadRuns", () => {
|
||||||
|
it("re-queues the latest cancelled or failed run without creating a duplicate message", async () => {
|
||||||
|
const transaction = {
|
||||||
|
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
|
||||||
|
run: {
|
||||||
|
findMany: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce([])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
id: "run-cancelled",
|
||||||
|
taskId: "task-cancelled",
|
||||||
|
botId: "bot-a",
|
||||||
|
status: "cancelled",
|
||||||
|
createdAt: new Date("2026-04-20T10:00:00.000Z"),
|
||||||
|
completedAt: new Date("2026-04-20T10:02:00.000Z"),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
findFirst: vi.fn().mockResolvedValue({ seq: 19 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const prisma = {
|
||||||
|
$transaction: vi.fn(async (fn: (tx: typeof transaction) => Promise<unknown>) =>
|
||||||
|
fn(transaction),
|
||||||
|
),
|
||||||
|
} as unknown as PrismaClient;
|
||||||
|
const jobs = {
|
||||||
|
enqueue: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const events = {
|
||||||
|
notify: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const actor = { spaceId: "workspace-1", userId: "user-1" } as Actor;
|
||||||
|
const target = {
|
||||||
|
kind: "bot",
|
||||||
|
botId: "bot-a",
|
||||||
|
threadId: "thread-1",
|
||||||
|
bot: {} as never,
|
||||||
|
} satisfies ThreadTarget;
|
||||||
|
|
||||||
|
const result = await resumeThreadRuns(
|
||||||
|
{
|
||||||
|
prisma,
|
||||||
|
events: events as never,
|
||||||
|
jobs: jobs as never,
|
||||||
|
},
|
||||||
|
actor,
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ ok: true, runIds: ["run-cancelled"] });
|
||||||
|
expect(transaction.task.updateMany).toHaveBeenCalledWith({
|
||||||
|
where: { id: "task-cancelled" },
|
||||||
|
data: { status: "queued" },
|
||||||
|
});
|
||||||
|
expect(transaction.run.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "run-cancelled" },
|
||||||
|
data: {
|
||||||
|
status: "queued",
|
||||||
|
error: null,
|
||||||
|
completedAt: null,
|
||||||
|
leaseOwner: null,
|
||||||
|
leaseExpiresAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(jobs.enqueue).toHaveBeenCalledWith({
|
||||||
|
name: "run.continue",
|
||||||
|
payload: { runId: "run-cancelled" },
|
||||||
|
replaceKey: "run:run-cancelled",
|
||||||
|
});
|
||||||
|
expect(events.notify).toHaveBeenCalledWith("thread-1", 19);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { ORPCError } from "@orpc/server";
|
import { ORPCError } from "@orpc/server";
|
||||||
import { type JobPublisher, runContinueJob, type SandboxProvider } from "@rakazo/adapter-kit";
|
import {
|
||||||
|
type JobPublisher,
|
||||||
|
runContinueJob,
|
||||||
|
runJobKey,
|
||||||
|
type SandboxProvider,
|
||||||
|
} from "@rakazo/adapter-kit";
|
||||||
import { cancelComputerRunWork, screenLeaseIdForRun, toComputerRef } from "@rakazo/adapters";
|
import { cancelComputerRunWork, screenLeaseIdForRun, toComputerRef } from "@rakazo/adapters";
|
||||||
import {
|
import {
|
||||||
type Actor,
|
type Actor,
|
||||||
@ -1151,51 +1156,27 @@ export async function reactToThreadMessage(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopThreadRuns(
|
async function snapshotCancelledRunTeardown(tx: Prisma.TransactionClient, runIds: string[]) {
|
||||||
deps: {
|
|
||||||
prisma: PrismaClient;
|
|
||||||
sandbox: SandboxProvider;
|
|
||||||
},
|
|
||||||
actor: Actor,
|
|
||||||
target: ThreadTarget,
|
|
||||||
) {
|
|
||||||
const { runIds, computers, leases } = await deps.prisma.$transaction(async (tx) => {
|
|
||||||
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
|
||||||
const cancelled = await tx.run.updateManyAndReturn({
|
|
||||||
where: {
|
|
||||||
threadId: target.threadId,
|
|
||||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
|
||||||
},
|
|
||||||
data: { status: "cancelled", completedAt: new Date() },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
const ids = cancelled.map((run) => run.id);
|
|
||||||
await tx.steeringMessage.deleteMany({
|
|
||||||
where: {
|
|
||||||
botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds },
|
|
||||||
message: { threadId: target.threadId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Snapshot teardown coordinates before commit. Once cancellation becomes
|
// Snapshot teardown coordinates before commit. Once cancellation becomes
|
||||||
// visible, a worker can release its lease / execution columns immediately; a
|
// visible, a worker can release its lease / execution columns immediately; a
|
||||||
// later lookup would then miss the sandbox work this request must stop.
|
// later lookup would then miss the sandbox work this request must stop.
|
||||||
// Team ownership lives on ComputerExecutionLease; Computer.executionRunId is
|
// Team ownership lives on ComputerExecutionLease; Computer.executionRunId is
|
||||||
// only a legacy secondary path and is not written by current acquisition.
|
// only a legacy secondary path and is not written by current acquisition.
|
||||||
const leases = ids.length
|
const leases = runIds.length
|
||||||
? await tx.computerExecutionLease.findMany({
|
? await tx.computerExecutionLease.findMany({
|
||||||
where: { runId: { in: ids } },
|
where: { runId: { in: runIds } },
|
||||||
select: { computerId: true, botId: true, runId: true, fence: true },
|
select: { computerId: true, botId: true, runId: true, fence: true },
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))];
|
const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))];
|
||||||
const computers = ids.length
|
const computers = runIds.length
|
||||||
? await tx.computer.findMany({
|
? await tx.computer.findMany({
|
||||||
where:
|
where:
|
||||||
leaseComputerIds.length > 0
|
leaseComputerIds.length > 0
|
||||||
? {
|
? {
|
||||||
OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: ids } }],
|
OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: runIds } }],
|
||||||
}
|
}
|
||||||
: { executionRunId: { in: ids } },
|
: { executionRunId: { in: runIds } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
homeKey: true,
|
homeKey: true,
|
||||||
@ -1206,8 +1187,19 @@ export async function stopThreadRuns(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
return { runIds: ids, computers, leases };
|
return { runIds, computers, leases };
|
||||||
});
|
}
|
||||||
|
|
||||||
|
async function teardownCancelledRuns(
|
||||||
|
deps: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
sandbox: SandboxProvider;
|
||||||
|
},
|
||||||
|
actor: Actor,
|
||||||
|
snapshot: Awaited<ReturnType<typeof snapshotCancelledRunTeardown>>,
|
||||||
|
) {
|
||||||
|
const { runIds, computers, leases } = snapshot;
|
||||||
|
if (runIds.length === 0) return;
|
||||||
// Keep the DB lease until after teardown so a replacement run cannot claim the
|
// Keep the DB lease until after teardown so a replacement run cannot claim the
|
||||||
// screen while we still need the cancelled run's screenLeaseId to release it.
|
// screen while we still need the cancelled run's screenLeaseId to release it.
|
||||||
const computerById = new Map(computers.map((computer) => [computer.id, computer]));
|
const computerById = new Map(computers.map((computer) => [computer.id, computer]));
|
||||||
@ -1285,6 +1277,381 @@ export async function stopThreadRuns(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function stopThreadRuns(
|
||||||
|
deps: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
sandbox: SandboxProvider;
|
||||||
|
},
|
||||||
|
actor: Actor,
|
||||||
|
target: ThreadTarget,
|
||||||
|
) {
|
||||||
|
const snapshot = await deps.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||||
|
const cancelled = await tx.run.updateManyAndReturn({
|
||||||
|
where: {
|
||||||
|
threadId: target.threadId,
|
||||||
|
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||||
|
},
|
||||||
|
data: { status: "cancelled", completedAt: new Date() },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const ids = cancelled.map((run) => run.id);
|
||||||
|
await tx.steeringMessage.deleteMany({
|
||||||
|
where: {
|
||||||
|
botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds },
|
||||||
|
message: { threadId: target.threadId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return snapshotCancelledRunTeardown(tx, ids);
|
||||||
|
});
|
||||||
|
await teardownCancelledRuns(deps, actor, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the latest stopped (`cancelled`) or interrupted (`failed`) run in the thread
|
||||||
|
* without creating a duplicate user message row.
|
||||||
|
*/
|
||||||
|
export async function resumeThreadRuns(
|
||||||
|
deps: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
events: ThreadEvents;
|
||||||
|
jobs: JobPublisher;
|
||||||
|
},
|
||||||
|
actor: Actor,
|
||||||
|
target: ThreadTarget,
|
||||||
|
) {
|
||||||
|
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||||
|
const committed = await deps.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||||
|
const activeRuns = await tx.run.findMany({
|
||||||
|
where: {
|
||||||
|
threadId: target.threadId,
|
||||||
|
botId: { in: botIds },
|
||||||
|
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||||
|
},
|
||||||
|
select: { id: true, status: true },
|
||||||
|
});
|
||||||
|
if (activeRuns.length > 0) {
|
||||||
|
return {
|
||||||
|
runs: activeRuns,
|
||||||
|
eventSeq: null as number | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentTerminals = await tx.run.findMany({
|
||||||
|
where: {
|
||||||
|
threadId: target.threadId,
|
||||||
|
botId: { in: botIds },
|
||||||
|
trigger: { not: "bot_message" },
|
||||||
|
status: { in: ["failed", "completed", "cancelled"] },
|
||||||
|
},
|
||||||
|
orderBy: [{ updatedAt: "desc" }, { id: "desc" }],
|
||||||
|
take: 20,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
botId: true,
|
||||||
|
taskId: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
completedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const latestTerminal = pickLatestTerminalRun(recentTerminals);
|
||||||
|
if (
|
||||||
|
!latestTerminal ||
|
||||||
|
(latestTerminal.status !== "cancelled" && latestTerminal.status !== "failed")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
runs: [] as Array<{ id: string; status: string }>,
|
||||||
|
eventSeq: null as number | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.task.updateMany({
|
||||||
|
where: { id: latestTerminal.taskId },
|
||||||
|
data: { status: "queued" },
|
||||||
|
});
|
||||||
|
await tx.run.update({
|
||||||
|
where: { id: latestTerminal.id },
|
||||||
|
data: {
|
||||||
|
status: "queued",
|
||||||
|
error: null,
|
||||||
|
completedAt: null,
|
||||||
|
leaseOwner: null,
|
||||||
|
leaseExpiresAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (target.kind === "group") {
|
||||||
|
await touchGroupUpdatedAt(tx, target.groupId);
|
||||||
|
}
|
||||||
|
const latestEvent = await tx.event.findFirst({
|
||||||
|
where: { threadId: target.threadId },
|
||||||
|
orderBy: { seq: "desc" },
|
||||||
|
select: { seq: true },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
runs: [{ id: latestTerminal.id, status: "queued" }],
|
||||||
|
eventSeq: latestEvent?.seq ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (committed.eventSeq !== null) {
|
||||||
|
await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => {
|
||||||
|
getLogger().error("thread resume realtime notification", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await enqueueRunsNeedingContinue(deps.jobs, committed.runs);
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
runIds: committed.runs.map((run) => run.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promote queued follow-up messages immediately: cancel any currently active runs
|
||||||
|
* holding up the queue without discarding unclaimed steering messages, create a
|
||||||
|
* new continuation run for the pending steering messages, and enqueue it right away.
|
||||||
|
*/
|
||||||
|
export async function sendQueuedThreadMessagesNow(
|
||||||
|
deps: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
sandbox: SandboxProvider;
|
||||||
|
events: ThreadEvents;
|
||||||
|
jobs: JobPublisher;
|
||||||
|
},
|
||||||
|
actor: Actor,
|
||||||
|
target: ThreadTarget,
|
||||||
|
input?: { messageId?: string },
|
||||||
|
) {
|
||||||
|
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||||
|
const committed = await deps.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||||
|
const pendingSteering = await tx.steeringMessage.findMany({
|
||||||
|
where: {
|
||||||
|
botId: { in: botIds },
|
||||||
|
claimedAt: null,
|
||||||
|
message: { threadId: target.threadId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
message: { select: { id: true, blocks: true, seq: true, createdAt: true } },
|
||||||
|
},
|
||||||
|
orderBy: [{ message: { seq: "asc" } }, { id: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendingByBotId = new Map<string, typeof pendingSteering>();
|
||||||
|
for (const item of pendingSteering) {
|
||||||
|
const list = pendingByBotId.get(item.botId) ?? [];
|
||||||
|
list.push(item);
|
||||||
|
pendingByBotId.set(item.botId, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingByBotId.size === 0 && input?.messageId) {
|
||||||
|
const targetMsg = await tx.message.findFirst({
|
||||||
|
where: { id: input.messageId, threadId: target.threadId, role: "user" },
|
||||||
|
select: { id: true, blocks: true, seq: true, createdAt: true },
|
||||||
|
});
|
||||||
|
if (targetMsg) {
|
||||||
|
const activeRuns = await tx.run.findMany({
|
||||||
|
where: {
|
||||||
|
threadId: target.threadId,
|
||||||
|
botId: { in: botIds },
|
||||||
|
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||||
|
createdAt: { lt: targetMsg.createdAt },
|
||||||
|
},
|
||||||
|
select: { id: true, botId: true },
|
||||||
|
});
|
||||||
|
const fallbackBotId =
|
||||||
|
activeRuns[0]?.botId ??
|
||||||
|
(target.kind === "bot" ? target.botId : groupDefaultBotId(target));
|
||||||
|
if (fallbackBotId && activeRuns.length > 0) {
|
||||||
|
const steering = await tx.steeringMessage.create({
|
||||||
|
data: {
|
||||||
|
messageId: targetMsg.id,
|
||||||
|
botId: fallbackBotId,
|
||||||
|
userId: actor.userId,
|
||||||
|
runId: activeRuns[0]?.id ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pendingByBotId.set(fallbackBotId, [{ ...steering, message: targetMsg }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingByBotId.size === 0) {
|
||||||
|
return {
|
||||||
|
teardown: { runIds: [] as string[], computers: [], leases: [] },
|
||||||
|
runs: [] as Array<{ id: string; status: string }>,
|
||||||
|
eventSeq: null as number | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const affectedBotIds = [...pendingByBotId.keys()];
|
||||||
|
const now = new Date();
|
||||||
|
const cancelled = await tx.run.updateManyAndReturn({
|
||||||
|
where: {
|
||||||
|
threadId: target.threadId,
|
||||||
|
botId: { in: affectedBotIds },
|
||||||
|
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||||
|
},
|
||||||
|
data: { status: "cancelled", completedAt: now },
|
||||||
|
select: { id: true, taskId: true },
|
||||||
|
});
|
||||||
|
const cancelledRunIds = cancelled.map((run) => run.id);
|
||||||
|
if (cancelled.length > 0) {
|
||||||
|
await tx.task.updateMany({
|
||||||
|
where: { id: { in: cancelled.map((run) => run.taskId) } },
|
||||||
|
data: { status: "cancelled" },
|
||||||
|
});
|
||||||
|
await tx.steeringMessage.deleteMany({
|
||||||
|
where: {
|
||||||
|
runId: { in: cancelledRunIds },
|
||||||
|
claimedAt: { not: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const teardown = await snapshotCancelledRunTeardown(tx, cancelledRunIds);
|
||||||
|
|
||||||
|
const createdRuns: Array<{ id: string; status: string }> = [];
|
||||||
|
for (const [botId, items] of pendingByBotId) {
|
||||||
|
const last = items.at(-1)!;
|
||||||
|
const task = await tx.task.create({
|
||||||
|
data: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
botId,
|
||||||
|
threadId: target.threadId,
|
||||||
|
userId: items[0]!.userId,
|
||||||
|
prompt: "Respond to the user's steering context.",
|
||||||
|
status: "queued",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const runCreatedAt = new Date(Math.max(Date.now(), last.message.createdAt.getTime() + 1));
|
||||||
|
const run = await tx.run.create({
|
||||||
|
data: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
botId,
|
||||||
|
threadId: target.threadId,
|
||||||
|
taskId: task.id,
|
||||||
|
userId: items[0]!.userId,
|
||||||
|
status: "queued",
|
||||||
|
trigger: "follow_up",
|
||||||
|
sourceMessageId: last.message.id,
|
||||||
|
createdAt: runCreatedAt,
|
||||||
|
},
|
||||||
|
select: { id: true, status: true },
|
||||||
|
});
|
||||||
|
await tx.steeringMessage.updateMany({
|
||||||
|
where: { id: { in: items.map((item) => item.id) } },
|
||||||
|
data: { runId: run.id, claimedAt: null },
|
||||||
|
});
|
||||||
|
await tx.message.updateMany({
|
||||||
|
where: { id: { in: items.map((item) => item.message.id) } },
|
||||||
|
data: { runId: run.id },
|
||||||
|
});
|
||||||
|
createdRuns.push(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.kind === "group") {
|
||||||
|
await touchGroupUpdatedAt(tx, target.groupId);
|
||||||
|
}
|
||||||
|
const latestEvent = await tx.event.findFirst({
|
||||||
|
where: { threadId: target.threadId },
|
||||||
|
orderBy: { seq: "desc" },
|
||||||
|
select: { seq: true },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
teardown,
|
||||||
|
runs: createdRuns,
|
||||||
|
eventSeq: latestEvent?.seq ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (committed.teardown.runIds.length > 0) {
|
||||||
|
await Promise.all([
|
||||||
|
teardownCancelledRuns(deps, actor, committed.teardown),
|
||||||
|
...committed.teardown.runIds.map((runId) =>
|
||||||
|
deps.jobs.cancel(runJobKey(runId)).catch(() => undefined),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (committed.runs.length > 0) {
|
||||||
|
await enqueueRunsNeedingContinue(deps.jobs, committed.runs);
|
||||||
|
if (committed.eventSeq != null) {
|
||||||
|
await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => {
|
||||||
|
getLogger().error("thread sendNow realtime notification", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
runIds: committed.runs.map((run) => run.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeQueuedMessage(
|
||||||
|
deps: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
events: ThreadEvents;
|
||||||
|
},
|
||||||
|
actor: Actor,
|
||||||
|
target: ThreadTarget,
|
||||||
|
messageId: string,
|
||||||
|
) {
|
||||||
|
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||||
|
const result = await deps.prisma.$transaction(async (tx) => {
|
||||||
|
// Verify the message exists and belongs to this thread
|
||||||
|
const message = await tx.message.findFirst({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
threadId: target.threadId,
|
||||||
|
role: "user",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
return { removed: false as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this message is queued (has an unclaimed steering message)
|
||||||
|
const steering = await tx.steeringMessage.findFirst({
|
||||||
|
where: {
|
||||||
|
messageId: messageId,
|
||||||
|
botId: { in: botIds },
|
||||||
|
claimedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!steering) {
|
||||||
|
// No queued steering entry — nothing to remove
|
||||||
|
return { removed: false as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the steering message (unclaims it from the queue)
|
||||||
|
await tx.steeringMessage.delete({
|
||||||
|
where: { id: steering.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also clean up the user message if it's exclusively queued
|
||||||
|
// (no run attached to it)
|
||||||
|
if (!message.runId) {
|
||||||
|
await tx.message.delete({
|
||||||
|
where: { id: messageId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { removed: true as const };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.removed) {
|
||||||
|
// Notify subscribers so the UI updates
|
||||||
|
const latest = await deps.prisma.event.findFirst({
|
||||||
|
where: { threadId: target.threadId },
|
||||||
|
orderBy: { seq: "desc" },
|
||||||
|
select: { seq: true },
|
||||||
|
});
|
||||||
|
await deps.events.notify(target.threadId, latest?.seq ?? 0).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true as const, removed: result.removed };
|
||||||
|
}
|
||||||
export async function setThreadUnreadState(
|
export async function setThreadUnreadState(
|
||||||
prisma: PrismaClient,
|
prisma: PrismaClient,
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
|
|||||||
@ -147,7 +147,11 @@ export async function persistVoiceCredential(
|
|||||||
let voiceId = input.voiceId?.trim() ?? "";
|
let voiceId = input.voiceId?.trim() ?? "";
|
||||||
if (!voiceId) {
|
if (!voiceId) {
|
||||||
const voices = await provider.listVoices(input.plaintext, voiceContext(actor, input.signal));
|
const voices = await provider.listVoices(input.plaintext, voiceContext(actor, input.signal));
|
||||||
voiceId = voices[0]?.id ?? "";
|
const germanVoice =
|
||||||
|
input.provider === "kokoro"
|
||||||
|
? voices.find((v) => v.id === "df_eva" || /^d[fm]_/i.test(v.id))
|
||||||
|
: undefined;
|
||||||
|
voiceId = germanVoice?.id ?? voices[0]?.id ?? "";
|
||||||
}
|
}
|
||||||
const stored = await deps.secrets.put(input.plaintext, voiceContext(actor, input.signal));
|
const stored = await deps.secrets.put(input.plaintext, voiceContext(actor, input.signal));
|
||||||
const cred = await withSerializableRetry(() =>
|
const cred = await withSerializableRetry(() =>
|
||||||
|
|||||||
@ -25,4 +25,12 @@ describe("requestWebOrigin", () => {
|
|||||||
"https://evoo.example",
|
"https://evoo.example",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts origins listed in extraTrustedOrigins", () => {
|
||||||
|
expect(
|
||||||
|
requestWebOrigin("https://host.example.ts.net:8443", configured, [
|
||||||
|
"https://host.example.ts.net:8443",
|
||||||
|
]),
|
||||||
|
).toBe("https://host.example.ts.net:8443");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,15 +14,20 @@ export function isLoopbackHost(host: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The web origin that browser-facing URLs (OAuth returns, screen links) should
|
/** The web origin that browser-facing URLs (OAuth returns, screen links) should
|
||||||
* point at for this request. A browser opened over a loopback or private-network
|
* point at for this request. A browser opened over a loopback, private-network,
|
||||||
* address gets its own origin back so a phone on the LAN is not sent to
|
* or explicitly trusted origin gets its own origin back so a phone on the LAN
|
||||||
* 127.0.0.1; any other caller gets the configured WEB_ORIGIN. */
|
* or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */
|
||||||
export function requestWebOrigin(origin: string | undefined, webOrigin: string): string {
|
export function requestWebOrigin(
|
||||||
|
origin: string | undefined,
|
||||||
|
webOrigin: string,
|
||||||
|
extraTrustedOrigins: readonly string[] = [],
|
||||||
|
): string {
|
||||||
if (!origin) return webOrigin;
|
if (!origin) return webOrigin;
|
||||||
try {
|
try {
|
||||||
const url = new URL(origin);
|
const url = new URL(origin);
|
||||||
if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin;
|
if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin;
|
||||||
if (url.origin === new URL(webOrigin).origin) return url.origin;
|
if (url.origin === new URL(webOrigin).origin) return url.origin;
|
||||||
|
if (extraTrustedOrigins.includes(url.origin)) return url.origin;
|
||||||
return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin;
|
return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin;
|
||||||
} catch {
|
} catch {
|
||||||
return webOrigin;
|
return webOrigin;
|
||||||
|
|||||||
@ -3,8 +3,15 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native";
|
import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native";
|
||||||
import { BotMemberPicker } from "../components/bot-member-picker";
|
import { BotMemberPicker } from "../components/bot-member-picker";
|
||||||
import { type MobileBot, type MobileGroup, rpc } from "../lib/api";
|
import {
|
||||||
|
type MobileBot,
|
||||||
|
type MobileGroup,
|
||||||
|
type MobileSpace,
|
||||||
|
type MobileSpaceNavigation,
|
||||||
|
rpc,
|
||||||
|
} from "../lib/api";
|
||||||
import { useI18n } from "../lib/i18n";
|
import { useI18n } from "../lib/i18n";
|
||||||
|
import { canDeleteInboxSpace, removeInboxSpace } from "../lib/inbox-spaces";
|
||||||
import { useMobileTokens } from "../lib/native";
|
import { useMobileTokens } from "../lib/native";
|
||||||
|
|
||||||
export default function GroupSettingsScreen() {
|
export default function GroupSettingsScreen() {
|
||||||
@ -13,12 +20,16 @@ export default function GroupSettingsScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { groupId } = useLocalSearchParams<{ groupId: string }>();
|
const { groupId } = useLocalSearchParams<{ groupId: string }>();
|
||||||
const [group, setGroup] = useState<MobileGroup | null>(null);
|
const [group, setGroup] = useState<MobileGroup | null>(null);
|
||||||
|
const [projectSpace, setProjectSpace] = useState<MobileSpace | null>(null);
|
||||||
const [bots, setBots] = useState<MobileBot[]>([]);
|
const [bots, setBots] = useState<MobileBot[]>([]);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pending, setPending] = useState(false);
|
const [pending, setPending] = useState(false);
|
||||||
|
|
||||||
|
const isMainConversation = group?.kind === "main";
|
||||||
|
const isProjectMain = isMainConversation && projectSpace !== null && !projectSpace.isDefault;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
@ -26,11 +37,19 @@ export default function GroupSettingsScreen() {
|
|||||||
(groups) => groups.find((row) => row.id === groupId) ?? null,
|
(groups) => groups.find((row) => row.id === groupId) ?? null,
|
||||||
),
|
),
|
||||||
rpc<MobileBot[]>("bots/list"),
|
rpc<MobileBot[]>("bots/list"),
|
||||||
|
rpc<MobileSpaceNavigation>("spaces/list").catch(() => null),
|
||||||
])
|
])
|
||||||
.then(([nextGroup, nextBots]) => {
|
.then(([nextGroup, nextBots, navigation]) => {
|
||||||
if (!nextGroup) throw new Error(t("Group not found"));
|
if (!nextGroup) throw new Error(t("Group not found"));
|
||||||
setGroup(nextGroup);
|
setGroup(nextGroup);
|
||||||
setName(nextGroup.name);
|
const space =
|
||||||
|
navigation?.spaces.find(
|
||||||
|
(item) => item.id === nextGroup.spaceId || item.id === navigation.current.id,
|
||||||
|
) ?? null;
|
||||||
|
setProjectSpace(space);
|
||||||
|
setName(
|
||||||
|
nextGroup.kind === "main" && space && !space.isDefault ? space.name : nextGroup.name,
|
||||||
|
);
|
||||||
setSelected(nextGroup.members.map((member) => member.botId));
|
setSelected(nextGroup.members.map((member) => member.botId));
|
||||||
setBots(nextBots.filter((bot) => !bot.archivedAt));
|
setBots(nextBots.filter((bot) => !bot.archivedAt));
|
||||||
})
|
})
|
||||||
@ -42,10 +61,20 @@ export default function GroupSettingsScreen() {
|
|||||||
setPending(true);
|
setPending(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (isProjectMain && projectSpace) {
|
||||||
|
if (trimmed && trimmed !== projectSpace.name) {
|
||||||
|
await rpc("spaces/update", { spaceId: projectSpace.id, name: trimmed });
|
||||||
|
}
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const input: { groupId: string; name?: string; botIds?: string[] } = { groupId };
|
const input: { groupId: string; name?: string; botIds?: string[] } = { groupId };
|
||||||
if (name.trim() !== group.name) input.name = name.trim();
|
if (trimmed !== group.name) input.name = trimmed;
|
||||||
|
if (!isMainConversation) {
|
||||||
const memberIds = group.members.map((member) => member.botId).join(",");
|
const memberIds = group.members.map((member) => member.botId).join(",");
|
||||||
if (selected.join(",") !== memberIds) input.botIds = selected;
|
if (selected.join(",") !== memberIds) input.botIds = selected;
|
||||||
|
}
|
||||||
if (input.name || input.botIds) await rpc("groups/update", input);
|
if (input.name || input.botIds) await rpc("groups/update", input);
|
||||||
router.back();
|
router.back();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -57,6 +86,29 @@ export default function GroupSettingsScreen() {
|
|||||||
|
|
||||||
function remove() {
|
function remove() {
|
||||||
if (!groupId || !group) return;
|
if (!groupId || !group) return;
|
||||||
|
if (isProjectMain && projectSpace) {
|
||||||
|
Alert.alert(
|
||||||
|
t("Delete {name}?", { name: projectSpace.name }),
|
||||||
|
t("This removes the empty space for everyone."),
|
||||||
|
[
|
||||||
|
{ text: t("Cancel"), style: "cancel" },
|
||||||
|
{
|
||||||
|
text: t("Delete"),
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () =>
|
||||||
|
void removeInboxSpace(projectSpace.id, async () => {})
|
||||||
|
.then(() => router.replace("/"))
|
||||||
|
.catch((err) =>
|
||||||
|
Alert.alert(
|
||||||
|
t("Could not delete space"),
|
||||||
|
err instanceof Error ? err.message : t("Try again."),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Alert.alert(group.name, t("Delete this group? Bots and their solo threads are kept."), [
|
Alert.alert(group.name, t("Delete this group? Bots and their solo threads are kept."), [
|
||||||
{ text: t("Cancel"), style: "cancel" },
|
{ text: t("Cancel"), style: "cancel" },
|
||||||
{
|
{
|
||||||
@ -75,6 +127,12 @@ export default function GroupSettingsScreen() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveDisabled =
|
||||||
|
!name.trim() ||
|
||||||
|
pending ||
|
||||||
|
(!isMainConversation &&
|
||||||
|
(selected.length < GROUP_MEMBER_MIN || selected.length > GROUP_MEMBER_MAX));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: t("Group settings") }} />
|
<Stack.Screen options={{ title: t("Group settings") }} />
|
||||||
@ -97,6 +155,8 @@ export default function GroupSettingsScreen() {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{!isMainConversation ? (
|
||||||
|
<>
|
||||||
<Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}>
|
<Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}>
|
||||||
{t("Members")}
|
{t("Members")}
|
||||||
</Text>
|
</Text>
|
||||||
@ -106,25 +166,16 @@ export default function GroupSettingsScreen() {
|
|||||||
onChange={setSelected}
|
onChange={setSelected}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{error ? <Text style={{ color: tokens.destructive, marginTop: 12 }}>{error}</Text> : null}
|
{error ? <Text style={{ color: tokens.destructive, marginTop: 12 }}>{error}</Text> : null}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => void save()}
|
onPress={() => void save()}
|
||||||
disabled={
|
disabled={saveDisabled}
|
||||||
!name.trim() ||
|
|
||||||
selected.length < GROUP_MEMBER_MIN ||
|
|
||||||
selected.length > GROUP_MEMBER_MAX ||
|
|
||||||
pending
|
|
||||||
}
|
|
||||||
style={{
|
style={{
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
backgroundColor: tokens.primary,
|
backgroundColor: tokens.primary,
|
||||||
opacity:
|
opacity: saveDisabled ? 0.5 : 1,
|
||||||
!name.trim() ||
|
|
||||||
selected.length < GROUP_MEMBER_MIN ||
|
|
||||||
selected.length > GROUP_MEMBER_MAX ||
|
|
||||||
pending
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
borderRadius: 11,
|
borderRadius: 11,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -134,6 +185,21 @@ export default function GroupSettingsScreen() {
|
|||||||
{pending ? t("Saving…") : t("Save")}
|
{pending ? t("Saving…") : t("Save")}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{isProjectMain && projectSpace && canDeleteInboxSpace(projectSpace) ? (
|
||||||
|
<Pressable
|
||||||
|
onPress={remove}
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
borderRadius: 11,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
padding: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete space")}</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : !isMainConversation ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={remove}
|
onPress={remove}
|
||||||
style={{
|
style={{
|
||||||
@ -147,6 +213,7 @@ export default function GroupSettingsScreen() {
|
|||||||
>
|
>
|
||||||
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text>
|
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
) : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -597,7 +597,21 @@ export default function Home() {
|
|||||||
}}
|
}}
|
||||||
disabled={spaceBusy}
|
disabled={spaceBusy}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (item.space) void chooseInboxSpace(item.space.id);
|
if (!item.space) return;
|
||||||
|
const targetSpace = item.space;
|
||||||
|
const main = !targetSpace.isDefault
|
||||||
|
? targetSpace.groups.find((group) => group.kind === "main")
|
||||||
|
: undefined;
|
||||||
|
if (main) {
|
||||||
|
void openMobileSpace(targetSpace.id, () =>
|
||||||
|
router.push({
|
||||||
|
pathname: "/group-thread",
|
||||||
|
params: { groupId: main.id, name: targetSpace.name },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void chooseInboxSpace(targetSpace.id);
|
||||||
}}
|
}}
|
||||||
style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]}
|
style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -6,22 +6,19 @@ import type {
|
|||||||
Connection,
|
Connection,
|
||||||
ConnectionCatalogItem,
|
ConnectionCatalogItem,
|
||||||
MessageBlock,
|
MessageBlock,
|
||||||
|
MessageReaction,
|
||||||
Routine,
|
Routine,
|
||||||
} from "@rakazo/contracts";
|
} from "@rakazo/contracts";
|
||||||
import {
|
import { AUTOPILOT_LIMITS, canReactToThreadMessage, MESSAGE_REACTIONS } from "@rakazo/contracts";
|
||||||
AUTOPILOT_LIMITS,
|
import type { AutopilotUntilChoice, ComposerMention, SlashActionId } from "@rakazo/core";
|
||||||
canReactToThreadMessage,
|
|
||||||
MESSAGE_REACTIONS,
|
|
||||||
type MessageReaction,
|
|
||||||
} from "@rakazo/contracts";
|
|
||||||
import type { AutopilotUntilChoice } from "@rakazo/core";
|
|
||||||
import {
|
import {
|
||||||
abortableDelay,
|
abortableDelay,
|
||||||
attachmentsForThread,
|
attachmentsForThread,
|
||||||
autopilotUntil,
|
autopilotUntil,
|
||||||
buildComposerMentionOptions,
|
buildComposerMentionOptions,
|
||||||
type ComposerMention,
|
clearComposerDraft,
|
||||||
cloudAgentHttpsUrl,
|
cloudAgentHttpsUrl,
|
||||||
|
composerDraftThreadKey,
|
||||||
deriveWorkspaceFocus,
|
deriveWorkspaceFocus,
|
||||||
foldRunNarration,
|
foldRunNarration,
|
||||||
isApprovalAskBlock,
|
isApprovalAskBlock,
|
||||||
@ -32,13 +29,15 @@ import {
|
|||||||
liveScreenRunId,
|
liveScreenRunId,
|
||||||
mentionChipKey,
|
mentionChipKey,
|
||||||
projectMessageReactions,
|
projectMessageReactions,
|
||||||
|
queuedFollowUps,
|
||||||
|
readComposerDraft,
|
||||||
resolveComposerSendPlan,
|
resolveComposerSendPlan,
|
||||||
SLASH_ACTIONS,
|
SLASH_ACTIONS,
|
||||||
type SlashActionId,
|
|
||||||
selectedAskActionLabel,
|
selectedAskActionLabel,
|
||||||
serializeComposerPrompt,
|
serializeComposerPrompt,
|
||||||
truncateSlashDescription,
|
truncateSlashDescription,
|
||||||
userVisibleMessages,
|
userVisibleMessages,
|
||||||
|
writeComposerDraft,
|
||||||
} from "@rakazo/core";
|
} from "@rakazo/core";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
||||||
@ -289,6 +288,8 @@ function Thread() {
|
|||||||
const mentionBotsAppliedGeneration = useRef(0);
|
const mentionBotsAppliedGeneration = useRef(0);
|
||||||
const readVisibleTarget = useRef<string | null>(null);
|
const readVisibleTarget = useRef<string | null>(null);
|
||||||
const threadKey = groupId ?? botId;
|
const threadKey = groupId ?? botId;
|
||||||
|
const draftKey = composerDraftThreadKey({ botId, groupId });
|
||||||
|
const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]);
|
||||||
const [threadScrollState, setThreadScrollState] = useState<ThreadScrollState>(() =>
|
const [threadScrollState, setThreadScrollState] = useState<ThreadScrollState>(() =>
|
||||||
scrollBehavior.current.state(),
|
scrollBehavior.current.state(),
|
||||||
);
|
);
|
||||||
@ -310,7 +311,7 @@ function Thread() {
|
|||||||
/** Run that last pointed at the bot's screen, so the live screen is offered only during real screen work. */
|
/** Run that last pointed at the bot's screen, so the live screen is offered only during real screen work. */
|
||||||
const [screenRunId, setScreenRunId] = useState<string | null>(null);
|
const [screenRunId, setScreenRunId] = useState<string | null>(null);
|
||||||
const activeThreadId = useRef<string | undefined>(undefined);
|
const activeThreadId = useRef<string | undefined>(undefined);
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState(() => initialDraft.text);
|
||||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||||
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
||||||
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
|
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
|
||||||
@ -325,12 +326,17 @@ function Thread() {
|
|||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]);
|
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>(
|
||||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null);
|
() => initialDraft.selectedMentions,
|
||||||
|
);
|
||||||
|
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
|
||||||
|
() => initialDraft.selectedSkill,
|
||||||
|
);
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
|
||||||
const [replyTarget, setReplyTarget] = useState<MobileMessage | null>(null);
|
const [replyTarget, setReplyTarget] = useState<MobileMessage | null>(null);
|
||||||
const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null);
|
const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
const [stopped, setStopped] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
const [markdownPreview, setMarkdownPreview] = useState<MarkdownArtifactPreviewTarget | null>(
|
const [markdownPreview, setMarkdownPreview] = useState<MarkdownArtifactPreviewTarget | null>(
|
||||||
@ -353,6 +359,9 @@ function Thread() {
|
|||||||
() => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)),
|
() => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)),
|
||||||
[reactionView.visibleMessages, runNarration],
|
[reactionView.visibleMessages, runNarration],
|
||||||
);
|
);
|
||||||
|
const [taskPillExpanded, setTaskPillExpanded] = useState(false);
|
||||||
|
const [dismissedPlanKey, setDismissedPlanKey] = useState<string | null>(null);
|
||||||
|
const [taskPillSubStepsOpen, setTaskPillSubStepsOpen] = useState(false);
|
||||||
const latestMessageId = visibleMessages.at(-1)?.id ?? null;
|
const latestMessageId = visibleMessages.at(-1)?.id ?? null;
|
||||||
const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey);
|
const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey);
|
||||||
const composerMentionTargets = useMemo(
|
const composerMentionTargets = useMemo(
|
||||||
@ -431,6 +440,106 @@ function Thread() {
|
|||||||
});
|
});
|
||||||
}, [inGroup, snap?.activeRuns, snap?.members, snap?.run]);
|
}, [inGroup, snap?.activeRuns, snap?.members, snap?.run]);
|
||||||
const working = inGroup ? workingGroupBots.length > 0 : isWorkingStatus(currentBotStatus);
|
const working = inGroup ? workingGroupBots.length > 0 : isWorkingStatus(currentBotStatus);
|
||||||
|
const activeMobilePlan = useMemo(() => {
|
||||||
|
const msgs = reactionView.visibleMessages;
|
||||||
|
let lastUserIdx = -1;
|
||||||
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||||
|
if (msgs[i]?.role === "user") {
|
||||||
|
lastUserIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let planIdx = -1;
|
||||||
|
let planBlock: Extract<MessageBlock, { kind: "plan" }> | null = null;
|
||||||
|
for (let i = msgs.length - 1; i > lastUserIdx; i--) {
|
||||||
|
const m = msgs[i];
|
||||||
|
if (!m) continue;
|
||||||
|
for (let b = m.blocks.length - 1; b >= 0; b--) {
|
||||||
|
const block = m.blocks[b];
|
||||||
|
if (block?.kind === "plan") {
|
||||||
|
planBlock = block;
|
||||||
|
planIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (planBlock) break;
|
||||||
|
}
|
||||||
|
if (!planBlock) {
|
||||||
|
for (let i = lastUserIdx - 1; i >= 0; i--) {
|
||||||
|
const m = msgs[i];
|
||||||
|
if (!m) continue;
|
||||||
|
for (let b = m.blocks.length - 1; b >= 0; b--) {
|
||||||
|
const block = m.blocks[b];
|
||||||
|
if (block?.kind === "plan" && block.status !== "completed") {
|
||||||
|
planBlock = block;
|
||||||
|
planIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (planBlock) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!planBlock) return null;
|
||||||
|
const absorbedIds = new Set<string>();
|
||||||
|
const stepIds = new Set<string>();
|
||||||
|
const steps: MobileMessage[] = [];
|
||||||
|
for (let i = Math.max(0, lastUserIdx + 1, planIdx); i < msgs.length; i++) {
|
||||||
|
const m = msgs[i];
|
||||||
|
if (!m) continue;
|
||||||
|
const folded = runNarration.steps.get(m.id);
|
||||||
|
if (folded && folded.length > 0) {
|
||||||
|
absorbedIds.add(m.id);
|
||||||
|
for (const s of folded) {
|
||||||
|
if (!stepIds.has(s.id)) {
|
||||||
|
stepIds.add(s.id);
|
||||||
|
steps.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const doneCount = planBlock.steps.filter(
|
||||||
|
(s) => s.status === "completed" || s.status === "skipped",
|
||||||
|
).length;
|
||||||
|
const isCompleted =
|
||||||
|
planBlock.status === "completed" ||
|
||||||
|
(planBlock.steps.length > 0 && doneCount === planBlock.steps.length && !working);
|
||||||
|
return {
|
||||||
|
block: planBlock,
|
||||||
|
steps,
|
||||||
|
absorbedIds,
|
||||||
|
doneCount,
|
||||||
|
isCompleted,
|
||||||
|
completionKey: `${planBlock.planId}:${isCompleted ? "done" : "active"}`,
|
||||||
|
};
|
||||||
|
}, [reactionView.visibleMessages, runNarration.steps, working]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeMobilePlan?.isCompleted) {
|
||||||
|
setDismissedPlanKey(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = activeMobilePlan.completionKey;
|
||||||
|
const collapseTimer = setTimeout(() => setTaskPillExpanded(false), 1800);
|
||||||
|
const hideTimer = setTimeout(() => setDismissedPlanKey(key), 3500);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(collapseTimer);
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
};
|
||||||
|
}, [activeMobilePlan?.completionKey, activeMobilePlan?.isCompleted]);
|
||||||
|
const activeRunCreatedAt = useMemo(() => {
|
||||||
|
const candidates = snap?.activeRuns ?? (snap?.run ? [snap.run] : []);
|
||||||
|
return candidates.find((run) => isWorkingStatus(run.status))?.createdAt;
|
||||||
|
}, [snap?.activeRuns, snap?.run]);
|
||||||
|
const queuedIds = useMemo(
|
||||||
|
() =>
|
||||||
|
new Set(
|
||||||
|
(working
|
||||||
|
? queuedFollowUps(snap?.messages ?? [], activeRunCreatedAt, reactionView.reactions)
|
||||||
|
: []
|
||||||
|
).map((message) => message.id),
|
||||||
|
),
|
||||||
|
[activeRunCreatedAt, reactionView.reactions, snap?.messages, working],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void rpc<AgentSkillCatalogEntry[]>("agentSkills/list")
|
void rpc<AgentSkillCatalogEntry[]>("agentSkills/list")
|
||||||
@ -1002,16 +1111,31 @@ function Thread() {
|
|||||||
);
|
);
|
||||||
}, [botId, groupId, messageId]);
|
}, [botId, groupId, messageId]);
|
||||||
|
|
||||||
|
const loadedDraftKeyRef = useRef(draftKey);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (loadedDraftKeyRef.current !== draftKey) {
|
||||||
|
loadedDraftKeyRef.current = draftKey;
|
||||||
|
const stored = readComposerDraft(draftKey);
|
||||||
|
setDraft(stored.text);
|
||||||
|
setSelectedSkill(stored.selectedSkill);
|
||||||
|
setSelectedMentions(stored.selectedMentions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeComposerDraft(draftKey, {
|
||||||
|
text: draft,
|
||||||
|
selectedSkill,
|
||||||
|
selectedMentions,
|
||||||
|
});
|
||||||
|
}, [draft, draftKey, selectedMentions, selectedSkill]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPendingAttachments((current) => attachmentsForThread(current, threadKey));
|
setPendingAttachments((current) => attachmentsForThread(current, threadKey));
|
||||||
setDraft("");
|
|
||||||
setMentionQuery(null);
|
setMentionQuery(null);
|
||||||
setSlashQuery(null);
|
setSlashQuery(null);
|
||||||
setSelectedSkill(null);
|
|
||||||
setSelectedMentions([]);
|
|
||||||
setReplyTarget(null);
|
setReplyTarget(null);
|
||||||
setAttachmentNotice(null);
|
setAttachmentNotice(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setStopped(false);
|
||||||
}, [threadKey]);
|
}, [threadKey]);
|
||||||
|
|
||||||
function updateDraft(value: string) {
|
function updateDraft(value: string) {
|
||||||
@ -1053,6 +1177,11 @@ function Thread() {
|
|||||||
function runSlashAction(action: SlashActionId) {
|
function runSlashAction(action: SlashActionId) {
|
||||||
setDraft("");
|
setDraft("");
|
||||||
setSlashQuery(null);
|
setSlashQuery(null);
|
||||||
|
writeComposerDraft(draftKey, {
|
||||||
|
text: "",
|
||||||
|
selectedSkill,
|
||||||
|
selectedMentions,
|
||||||
|
});
|
||||||
if (action === "chat-settings") {
|
if (action === "chat-settings") {
|
||||||
if (inGroup && groupId) {
|
if (inGroup && groupId) {
|
||||||
router.push({ pathname: "/group-settings", params: { groupId } });
|
router.push({ pathname: "/group-settings", params: { groupId } });
|
||||||
@ -1078,6 +1207,10 @@ function Thread() {
|
|||||||
const initialGroupTarget = groupId;
|
const initialGroupTarget = groupId;
|
||||||
if ((!initialBotTarget && !initialGroupTarget) || sending) return;
|
if ((!initialBotTarget && !initialGroupTarget) || sending) return;
|
||||||
const originThreadKey = initialGroupTarget ?? initialBotTarget;
|
const originThreadKey = initialGroupTarget ?? initialBotTarget;
|
||||||
|
const originDraftKey = composerDraftThreadKey({
|
||||||
|
botId: initialBotTarget,
|
||||||
|
groupId: initialGroupTarget,
|
||||||
|
});
|
||||||
const attachments = attachmentsForThread(pendingAttachments, originThreadKey);
|
const attachments = attachmentsForThread(pendingAttachments, originThreadKey);
|
||||||
const plan = resolveComposerSendPlan({
|
const plan = resolveComposerSendPlan({
|
||||||
text: serializeComposerPromptText(),
|
text: serializeComposerPromptText(),
|
||||||
@ -1097,6 +1230,7 @@ function Thread() {
|
|||||||
if (initialBotTarget) cancelFocusPrompt(initialBotTarget);
|
if (initialBotTarget) cancelFocusPrompt(initialBotTarget);
|
||||||
};
|
};
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
setStopped(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
if (plan.shouldRunRoutines) {
|
if (plan.shouldRunRoutines) {
|
||||||
@ -1111,6 +1245,7 @@ function Thread() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const clearOriginComposer = () => {
|
const clearOriginComposer = () => {
|
||||||
|
clearComposerDraft(originDraftKey);
|
||||||
setPendingAttachments((current) =>
|
setPendingAttachments((current) =>
|
||||||
current.filter((attachment) => attachment.threadKey !== originThreadKey),
|
current.filter((attachment) => attachment.threadKey !== originThreadKey),
|
||||||
);
|
);
|
||||||
@ -1205,12 +1340,16 @@ function Thread() {
|
|||||||
const targetGroupId = groupId;
|
const targetGroupId = groupId;
|
||||||
if ((!targetBotId && !targetGroupId) || sending) return;
|
if ((!targetBotId && !targetGroupId) || sending) return;
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
setStopped(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await rpc(
|
await rpc(
|
||||||
"threads/stop",
|
"threads/stop",
|
||||||
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
|
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
|
||||||
);
|
);
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
setStopped(true);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
setError(err instanceof Error ? err.message : t("Failed to stop work"));
|
setError(err instanceof Error ? err.message : t("Failed to stop work"));
|
||||||
@ -1230,6 +1369,80 @@ function Thread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resume() {
|
||||||
|
const targetBotId = botId;
|
||||||
|
const targetGroupId = groupId;
|
||||||
|
if ((!targetBotId && !targetGroupId) || sending) return;
|
||||||
|
setSending(true);
|
||||||
|
setStopped(false);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await rpc(
|
||||||
|
"threads/resume",
|
||||||
|
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
|
||||||
|
);
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
setError(err instanceof Error ? err.message : t("Could not continue"));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sendingNowId, setSendingNowId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function sendQueuedNow(message: MobileMessage) {
|
||||||
|
const targetBotId = botId;
|
||||||
|
const targetGroupId = groupId;
|
||||||
|
if ((!targetBotId && !targetGroupId) || sendingNowId) return;
|
||||||
|
setSendingNowId(message.id);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await rpc("threads/sendNow", {
|
||||||
|
...(targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }),
|
||||||
|
messageId: message.id,
|
||||||
|
});
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
setError(err instanceof Error ? err.message : t("Failed to send message"));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSendingNowId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [removingQueuedId, setRemovingQueuedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function removeQueuedNow(message: MobileMessage) {
|
||||||
|
const targetBotId = botId;
|
||||||
|
const targetGroupId = groupId;
|
||||||
|
const messageId = message.id.startsWith("temp-") ? undefined : message.id;
|
||||||
|
if (!messageId || (!targetBotId && !targetGroupId) || removingQueuedId) return;
|
||||||
|
setRemovingQueuedId(message.id);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await rpc("threads/removeQueuedMessage", {
|
||||||
|
...(targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }),
|
||||||
|
messageId,
|
||||||
|
});
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
|
setError(err instanceof Error ? err.message : t("Failed to remove message"));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setRemovingQueuedId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
const answerMessage = useCallback(
|
const answerMessage = useCallback(
|
||||||
async (message: MobileMessage, answer: string) => {
|
async (message: MobileMessage, answer: string) => {
|
||||||
const targetBotId = botId;
|
const targetBotId = botId;
|
||||||
@ -1313,6 +1526,10 @@ function Thread() {
|
|||||||
const targetGroupId = groupId;
|
const targetGroupId = groupId;
|
||||||
if (!targetBotId && !targetGroupId) return;
|
if (!targetBotId && !targetGroupId) return;
|
||||||
const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! };
|
const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! };
|
||||||
|
const targetDraftKey = composerDraftThreadKey({
|
||||||
|
botId: targetBotId,
|
||||||
|
groupId: targetGroupId,
|
||||||
|
});
|
||||||
const until = autopilotUntil(choice, new Date());
|
const until = autopilotUntil(choice, new Date());
|
||||||
void rpc("autopilot/start", {
|
void rpc("autopilot/start", {
|
||||||
...target,
|
...target,
|
||||||
@ -1320,8 +1537,12 @@ function Thread() {
|
|||||||
...(until ? { until: until.toISOString() } : {}),
|
...(until ? { until: until.toISOString() } : {}),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
const existing = readComposerDraft(targetDraftKey);
|
||||||
|
writeComposerDraft(targetDraftKey, { ...existing, text: "" });
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||||
setDraft("");
|
setDraft("");
|
||||||
if (isCurrentTarget(targetBotId, targetGroupId)) await refresh();
|
await refresh();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")),
|
Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")),
|
||||||
@ -1553,7 +1774,9 @@ function Thread() {
|
|||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{foldedSteps ? <RunSteps steps={foldedSteps} /> : null}
|
{foldedSteps && !activeMobilePlan?.absorbedIds.has(message.id) ? (
|
||||||
|
<RunSteps steps={foldedSteps} />
|
||||||
|
) : null}
|
||||||
<Pressable accessible={false} onLongPress={actionProps.onLongPress}>
|
<Pressable accessible={false} onLongPress={actionProps.onLongPress}>
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
botId={botId ?? snap?.members?.[0]?.botId ?? ""}
|
botId={botId ?? snap?.members?.[0]?.botId ?? ""}
|
||||||
@ -1574,6 +1797,55 @@ function Thread() {
|
|||||||
actionProps={actionProps}
|
actionProps={actionProps}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{queuedIds.has(message.id) ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.mutedForeground, fontSize: 12 }}>{t("Queued")}</Text>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
disabled={sendingNowId === message.id}
|
||||||
|
hitSlop={6}
|
||||||
|
onPress={() => void sendQueuedNow(message)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: tokens.foreground,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
opacity: sendingNowId === message.id ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("Send now")}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
disabled={removingQueuedId === message.id}
|
||||||
|
hitSlop={6}
|
||||||
|
onPress={() => void removeQueuedNow(message)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
removingQueuedId === message.id ? tokens.mutedForeground : tokens.destructive,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
opacity: removingQueuedId === message.id ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{t("Remove")}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{messageReactions ? (
|
{messageReactions ? (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -1684,7 +1956,70 @@ function Thread() {
|
|||||||
>
|
>
|
||||||
{error ? <Text style={{ color: tokens.mutedForeground, marginTop: 12 }}>{error}</Text> : null}
|
{error ? <Text style={{ color: tokens.mutedForeground, marginTop: 12 }}>{error}</Text> : null}
|
||||||
{runError ? (
|
{runError ? (
|
||||||
<Text style={{ color: tokens.destructive, marginTop: 12 }}>{runError}</Text>
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.destructive, flex: 1 }}>{runError}</Text>
|
||||||
|
{!working ? (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
testID="thread-resume-button"
|
||||||
|
disabled={sending}
|
||||||
|
onPress={() => void resume()}
|
||||||
|
style={{
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
backgroundColor: tokens.card,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
opacity: sending ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.foreground, fontSize: 13, fontWeight: "600" }}>
|
||||||
|
{t("Resume")}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : stopped && !working ? (
|
||||||
|
<View
|
||||||
|
testID="thread-stopped-banner"
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.mutedForeground, flex: 1 }}>{t("Stopped")}</Text>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
testID="thread-resume-button"
|
||||||
|
disabled={sending}
|
||||||
|
onPress={() => void resume()}
|
||||||
|
style={{
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
backgroundColor: tokens.card,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
opacity: sending ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.foreground, fontSize: 13, fontWeight: "600" }}>
|
||||||
|
{t("Resume")}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={{ flex: 1, position: "relative" }}>
|
<View style={{ flex: 1, position: "relative" }}>
|
||||||
{showPinnedPage ? (
|
{showPinnedPage ? (
|
||||||
@ -2025,6 +2360,191 @@ function Thread() {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
{activeMobilePlan &&
|
||||||
|
!(activeMobilePlan.isCompleted && dismissedPlanKey === activeMobilePlan.completionKey) ? (
|
||||||
|
<View
|
||||||
|
testID="composer-task-pill"
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
backgroundColor: tokens.card,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 10,
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
testID="composer-task-pill-toggle"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ expanded: taskPillExpanded }}
|
||||||
|
onPress={() => setTaskPillExpanded((current) => !current)}
|
||||||
|
style={{ gap: 6 }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: tokens.border,
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: activeMobilePlan.isCompleted ? tokens.success : tokens.foreground,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "700",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeMobilePlan.isCompleted ? "✓" : "▸"}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={{
|
||||||
|
color: tokens.foreground,
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: "600",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeMobilePlan.isCompleted
|
||||||
|
? activeMobilePlan.block.title
|
||||||
|
: ((
|
||||||
|
activeMobilePlan.block.steps.find((s) => s.status === "in_progress") ??
|
||||||
|
activeMobilePlan.block.steps.find((s) => s.status === "pending")
|
||||||
|
)?.label ?? activeMobilePlan.block.title)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: activeMobilePlan.isCompleted ? tokens.success : tokens.mutedForeground,
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeMobilePlan.isCompleted
|
||||||
|
? t("Completed")
|
||||||
|
: `${activeMobilePlan.doneCount}/${activeMobilePlan.block.steps.length}`}
|
||||||
|
</Text>
|
||||||
|
<NativeSymbol
|
||||||
|
ios={taskPillExpanded ? "chevron.down" : "chevron.up"}
|
||||||
|
android={taskPillExpanded ? "chevron-down" : "chevron-up"}
|
||||||
|
size={14}
|
||||||
|
color={tokens.mutedForeground}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
{taskPillExpanded ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: tokens.border,
|
||||||
|
paddingTop: 10,
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: tokens.cardForeground,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeMobilePlan.block.title}
|
||||||
|
</Text>
|
||||||
|
{activeMobilePlan.block.steps.map((step) => {
|
||||||
|
const done = step.status === "completed";
|
||||||
|
const active = step.status === "in_progress";
|
||||||
|
const skipped = step.status === "skipped";
|
||||||
|
const glyph = done ? "✓" : active ? "▸" : skipped ? "–" : "○";
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={step.id}
|
||||||
|
style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: done
|
||||||
|
? tokens.success
|
||||||
|
: active
|
||||||
|
? tokens.foreground
|
||||||
|
: tokens.mutedForeground,
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: active ? "700" : "400",
|
||||||
|
width: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{glyph}
|
||||||
|
</Text>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: done || skipped ? tokens.mutedForeground : tokens.foreground,
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: active ? "600" : "400",
|
||||||
|
textDecorationLine: done || skipped ? "line-through" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.label}
|
||||||
|
</Text>
|
||||||
|
{step.note ? (
|
||||||
|
<Text style={{ color: tokens.mutedForeground, fontSize: 12 }}>
|
||||||
|
{step.note}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{activeMobilePlan.steps.length > 0 ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: tokens.border,
|
||||||
|
paddingTop: 6,
|
||||||
|
marginTop: 2,
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => setTaskPillSubStepsOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.mutedForeground, fontSize: 12.5 }}>
|
||||||
|
{t("{count} steps", { count: activeMobilePlan.steps.length })}{" "}
|
||||||
|
{taskPillSubStepsOpen ? "▴" : "▾"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
{taskPillSubStepsOpen
|
||||||
|
? activeMobilePlan.steps.map((s) => (
|
||||||
|
<Text
|
||||||
|
key={s.id}
|
||||||
|
style={{ color: tokens.mutedForeground, fontSize: 12.5 }}
|
||||||
|
>
|
||||||
|
{blockText(s)}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@ -2528,6 +3048,105 @@ const MessageBubble = memo(function MessageBubble({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const plans = message.blocks.filter(
|
||||||
|
(block): block is Extract<MessageBlock, { kind: "plan" }> => block.kind === "plan",
|
||||||
|
);
|
||||||
|
if (plans.length) {
|
||||||
|
return (
|
||||||
|
<View style={{ gap: 8, width: "100%" }}>
|
||||||
|
{plans.map((plan) => {
|
||||||
|
const doneCount = plan.steps.filter(
|
||||||
|
(s) => s.status === "completed" || s.status === "skipped",
|
||||||
|
).length;
|
||||||
|
const isCompleted = plan.status === "completed";
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={plan.planId}
|
||||||
|
testID="plan-card"
|
||||||
|
style={{
|
||||||
|
width: "90%",
|
||||||
|
borderRadius: 18,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
backgroundColor: tokens.card,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: tokens.cardForeground,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "600",
|
||||||
|
flexShrink: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{plan.title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: isCompleted ? tokens.success : tokens.mutedForeground,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCompleted ? t("Completed") : `${doneCount}/${plan.steps.length}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ gap: 6 }}>
|
||||||
|
{plan.steps.map((step) => {
|
||||||
|
const done = step.status === "completed";
|
||||||
|
const active = step.status === "in_progress";
|
||||||
|
const skipped = step.status === "skipped";
|
||||||
|
const glyph = done ? "✓" : active ? "▸" : skipped ? "–" : "○";
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={step.id}
|
||||||
|
style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: done
|
||||||
|
? tokens.success
|
||||||
|
: active
|
||||||
|
? tokens.foreground
|
||||||
|
: tokens.mutedForeground,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: active ? "700" : "400",
|
||||||
|
width: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{glyph}
|
||||||
|
</Text>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: done || skipped ? tokens.mutedForeground : tokens.foreground,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: active ? "600" : "400",
|
||||||
|
textDecorationLine: done || skipped ? "line-through" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.label}
|
||||||
|
</Text>
|
||||||
|
{step.note ? (
|
||||||
|
<Text style={{ color: tokens.mutedForeground, fontSize: 12.5 }}>
|
||||||
|
{step.note}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isRoutineEventMessage(message.blocks)) {
|
if (isRoutineEventMessage(message.blocks)) {
|
||||||
return (
|
return (
|
||||||
<View style={{ width: "100%" }}>
|
<View style={{ width: "100%" }}>
|
||||||
|
|||||||
@ -784,7 +784,7 @@ export type MobileGroup = Pick<
|
|||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "members"
|
| "members"
|
||||||
> &
|
> &
|
||||||
Partial<Pick<Group, "spaceId">>;
|
Partial<Pick<Group, "spaceId" | "kind">>;
|
||||||
|
|
||||||
export type MobileSpace = Space;
|
export type MobileSpace = Space;
|
||||||
export type MobileSpaceNavigation = SpaceNavigation;
|
export type MobileSpaceNavigation = SpaceNavigation;
|
||||||
@ -797,8 +797,14 @@ export type MobileSnapshot = {
|
|||||||
cursor?: number;
|
cursor?: number;
|
||||||
messages: MobileMessage[];
|
messages: MobileMessage[];
|
||||||
olderCursor: number | null;
|
olderCursor: number | null;
|
||||||
run: { id: string; botId?: string; status: string; error?: string | null } | null;
|
run: {
|
||||||
activeRuns?: Array<{ id: string; botId?: string; status: string }>;
|
id: string;
|
||||||
|
botId?: string;
|
||||||
|
status: string;
|
||||||
|
error?: string | null;
|
||||||
|
createdAt?: string;
|
||||||
|
} | null;
|
||||||
|
activeRuns?: Array<{ id: string; botId?: string; status: string; createdAt?: string }>;
|
||||||
members?: MobileGroup["members"];
|
members?: MobileGroup["members"];
|
||||||
computer?: {
|
computer?: {
|
||||||
kind?: string;
|
kind?: string;
|
||||||
|
|||||||
@ -147,12 +147,18 @@ describe("spaceInboxItems", () => {
|
|||||||
expect(canDeleteInboxSpace(space({ canDelete }))).toBe(false);
|
expect(canDeleteInboxSpace(space({ canDelete }))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("offers deletion for an explicitly authorized empty non-default space", () => {
|
it("offers deletion for an explicitly authorized non-default space", () => {
|
||||||
expect(canDeleteInboxSpace(space())).toBe(true);
|
expect(canDeleteInboxSpace(space())).toBe(true);
|
||||||
|
expect(canDeleteInboxSpace(space({ hasContent: true, canDelete: true }))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides deletion while a space still has content", () => {
|
it("deduplicates same-named projects into a single project entry", () => {
|
||||||
expect(canDeleteInboxSpace(space({ hasContent: true }))).toBe(false);
|
const first = space({ id: "space-work-1", name: "Work", hasContent: false });
|
||||||
|
const second = space({ id: "space-work-2", name: " work ", hasContent: true, bots: [bot] });
|
||||||
|
const items = spaceInboxItems([first, second]);
|
||||||
|
const headings = items.filter((item) => item.type === "heading" && item.space);
|
||||||
|
expect(headings).toHaveLength(1);
|
||||||
|
expect(headings[0]).toMatchObject({ key: "space-work-2", title: " work " });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,22 +24,47 @@ export type InboxSpaceItem =
|
|||||||
| { type: "heading"; key: string; title: string; space?: InboxSpace };
|
| { type: "heading"; key: string; title: string; space?: InboxSpace };
|
||||||
|
|
||||||
export function canDeleteInboxSpace(
|
export function canDeleteInboxSpace(
|
||||||
space: Pick<InboxSpace, "isDefault" | "hasContent" | "canDelete">,
|
space: Pick<InboxSpace, "isDefault" | "canDelete"> & Partial<Pick<InboxSpace, "hasContent">>,
|
||||||
) {
|
) {
|
||||||
return !space.isDefault && !space.hasContent && space.canDelete === true;
|
return !space.isDefault && space.canDelete === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
|
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
|
||||||
return spaces.flatMap((space): InboxSpaceItem[] => {
|
const seenProjectNames = new Set<string>();
|
||||||
|
const canonicalProjectIds = new Set<string>();
|
||||||
|
const orderedProjects = [...spaces]
|
||||||
|
.filter((space) => !space.isDefault)
|
||||||
|
.sort((left, right) => Number(Boolean(right.hasContent)) - Number(Boolean(left.hasContent)));
|
||||||
|
for (const space of orderedProjects) {
|
||||||
|
const normalized = space.name.trim().toLowerCase();
|
||||||
|
if (normalized && seenProjectNames.has(normalized)) continue;
|
||||||
|
if (normalized) seenProjectNames.add(normalized);
|
||||||
|
canonicalProjectIds.add(space.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleSpaces = spaces.filter(
|
||||||
|
(space) => space.isDefault || canonicalProjectIds.has(space.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return visibleSpaces.flatMap((space): InboxSpaceItem[] => {
|
||||||
|
const normalizedSpaceName = space.name.trim().toLowerCase();
|
||||||
const chats = [
|
const chats = [
|
||||||
// A main bot is reached through its space's main conversation, not listed again.
|
// A main bot is reached through its space's main conversation, not listed again.
|
||||||
...space.bots
|
...space.bots
|
||||||
.filter((chat) => chat.kind !== "main")
|
.filter((chat) => chat.kind !== "main")
|
||||||
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
|
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
|
||||||
...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
|
...space.groups
|
||||||
|
.filter((chat) => {
|
||||||
|
const normalizedGroupName = chat.name.trim().toLowerCase();
|
||||||
|
if (!space.isDefault && normalizedGroupName === normalizedSpaceName) return false;
|
||||||
|
if (space.isDefault && chat.kind !== "main" && seenProjectNames.has(normalizedGroupName))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
|
||||||
];
|
];
|
||||||
const items: InboxSpaceItem[] = [];
|
const items: InboxSpaceItem[] = [];
|
||||||
if (spaces.length > 1 || !space.isDefault || chats.length === 0) {
|
if (visibleSpaces.length > 1 || !space.isDefault || chats.length === 0) {
|
||||||
items.push({ type: "heading", key: space.id, title: space.name, space });
|
items.push({ type: "heading", key: space.id, title: space.name, space });
|
||||||
}
|
}
|
||||||
for (const group of groupBotsForSidebar(chats, space.botSections)) {
|
for (const group of groupBotsForSidebar(chats, space.botSections)) {
|
||||||
|
|||||||
@ -261,6 +261,9 @@ export const DE_MESSAGES: Record<string, string> = {
|
|||||||
Eyeshadow: "Lidschatten",
|
Eyeshadow: "Lidschatten",
|
||||||
Failed: "Fehlgeschlagen",
|
Failed: "Fehlgeschlagen",
|
||||||
"Failed to refresh": "Aktualisieren fehlgeschlagen",
|
"Failed to refresh": "Aktualisieren fehlgeschlagen",
|
||||||
|
Resume: "Fortsetzen",
|
||||||
|
Stopped: "Gestoppt",
|
||||||
|
"Failed to remove message": "Nachricht konnte nicht entfernt werden",
|
||||||
"Failed to send message": "Nachricht konnte nicht gesendet werden",
|
"Failed to send message": "Nachricht konnte nicht gesendet werden",
|
||||||
"Failed to stop work": "Arbeit konnte nicht gestoppt werden",
|
"Failed to stop work": "Arbeit konnte nicht gestoppt werden",
|
||||||
"Fewer {label}": "Weniger {label}",
|
"Fewer {label}": "Weniger {label}",
|
||||||
@ -497,6 +500,7 @@ export const DE_MESSAGES: Record<string, string> = {
|
|||||||
"Section name": "Abschnittsname",
|
"Section name": "Abschnittsname",
|
||||||
Send: "Senden",
|
Send: "Senden",
|
||||||
"Send answer": "Antwort senden",
|
"Send answer": "Antwort senden",
|
||||||
|
"Send now": "Jetzt gleich senden",
|
||||||
"Send reset link": "Link zum Zurücksetzen senden",
|
"Send reset link": "Link zum Zurücksetzen senden",
|
||||||
"Sending…": "Wird gesendet…",
|
"Sending…": "Wird gesendet…",
|
||||||
Server: "Server",
|
Server: "Server",
|
||||||
|
|||||||
@ -274,6 +274,9 @@ export const RU_MESSAGES: Record<string, string> = {
|
|||||||
"Executor token": "Токен Executor",
|
"Executor token": "Токен Executor",
|
||||||
Failed: "Ошибка",
|
Failed: "Ошибка",
|
||||||
"Failed to refresh": "Не удалось обновить",
|
"Failed to refresh": "Не удалось обновить",
|
||||||
|
Resume: "Продолжить",
|
||||||
|
Stopped: "Остановлено",
|
||||||
|
"Failed to remove message": "Не удалось удалить сообщение",
|
||||||
"Failed to send message": "Не удалось отправить сообщение",
|
"Failed to send message": "Не удалось отправить сообщение",
|
||||||
"Failed to stop work": "Не удалось остановить работу",
|
"Failed to stop work": "Не удалось остановить работу",
|
||||||
File: "Файл",
|
File: "Файл",
|
||||||
@ -429,6 +432,7 @@ export const RU_MESSAGES: Record<string, string> = {
|
|||||||
Send: "Отправить",
|
Send: "Отправить",
|
||||||
"Send reset link": "Отправить ссылку для сброса",
|
"Send reset link": "Отправить ссылку для сброса",
|
||||||
"Send answer": "Отправить ответ",
|
"Send answer": "Отправить ответ",
|
||||||
|
"Send now": "Отправить сейчас",
|
||||||
"Sending…": "Отправка…",
|
"Sending…": "Отправка…",
|
||||||
Server: "Сервер",
|
Server: "Сервер",
|
||||||
"Server URL": "URL-адрес сервера",
|
"Server URL": "URL-адрес сервера",
|
||||||
|
|||||||
@ -259,6 +259,9 @@ export const ZH_MESSAGES: Record<string, string> = {
|
|||||||
"Executor token": "Executor 令牌",
|
"Executor token": "Executor 令牌",
|
||||||
Failed: "失败",
|
Failed: "失败",
|
||||||
"Failed to refresh": "刷新失败",
|
"Failed to refresh": "刷新失败",
|
||||||
|
Resume: "继续",
|
||||||
|
Stopped: "已停止",
|
||||||
|
"Failed to remove message": "删除消息失败",
|
||||||
"Failed to send message": "发送消息失败",
|
"Failed to send message": "发送消息失败",
|
||||||
"Failed to stop work": "停止工作失败",
|
"Failed to stop work": "停止工作失败",
|
||||||
File: "文件",
|
File: "文件",
|
||||||
@ -410,6 +413,7 @@ export const ZH_MESSAGES: Record<string, string> = {
|
|||||||
Send: "发送",
|
Send: "发送",
|
||||||
"Send reset link": "发送重置链接",
|
"Send reset link": "发送重置链接",
|
||||||
"Send answer": "发送回答",
|
"Send answer": "发送回答",
|
||||||
|
"Send now": "立即发送",
|
||||||
"Sending…": "正在发送…",
|
"Sending…": "正在发送…",
|
||||||
Server: "服务器",
|
Server: "服务器",
|
||||||
"Server URL": "服务器地址",
|
"Server URL": "服务器地址",
|
||||||
|
|||||||
@ -18,14 +18,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "57.0.17",
|
"@expo/metro-runtime": "57.0.17",
|
||||||
"@expo/ui": "57.0.25",
|
"@expo/ui": "57.0.26",
|
||||||
"@rakazo/chat-ui": "workspace:*",
|
"@rakazo/chat-ui": "workspace:*",
|
||||||
"@rakazo/contracts": "workspace:*",
|
"@rakazo/contracts": "workspace:*",
|
||||||
"@rakazo/core": "workspace:*",
|
"@rakazo/core": "workspace:*",
|
||||||
"@rakazo/ui-tokens": "workspace:*",
|
"@rakazo/ui-tokens": "workspace:*",
|
||||||
"@react-native-vector-icons/ionicons": "13.1.4",
|
"@react-native-vector-icons/ionicons": "13.1.4",
|
||||||
"@react-native-vector-icons/material-design-icons": "13.1.4",
|
"@react-native-vector-icons/material-design-icons": "13.1.4",
|
||||||
"expo": "57.0.30",
|
"expo": "57.0.31",
|
||||||
"expo-application": "57.0.3",
|
"expo-application": "57.0.3",
|
||||||
"expo-audio": "57.0.5",
|
"expo-audio": "57.0.5",
|
||||||
"expo-clipboard": "57.0.2",
|
"expo-clipboard": "57.0.2",
|
||||||
@ -37,11 +37,11 @@
|
|||||||
"expo-keep-awake": "57.0.2",
|
"expo-keep-awake": "57.0.2",
|
||||||
"expo-linking": "57.0.13",
|
"expo-linking": "57.0.13",
|
||||||
"expo-localization": "57.0.2",
|
"expo-localization": "57.0.2",
|
||||||
"expo-modules-core": "57.0.23",
|
"expo-modules-core": "57.0.24",
|
||||||
"expo-notifications": "57.0.23",
|
"expo-notifications": "57.0.24",
|
||||||
"expo-router": "57.0.28",
|
"expo-router": "57.0.29",
|
||||||
"expo-secure-store": "57.0.4",
|
"expo-secure-store": "57.0.4",
|
||||||
"expo-sharing": "57.0.23",
|
"expo-sharing": "57.0.24",
|
||||||
"expo-splash-screen": "57.0.10",
|
"expo-splash-screen": "57.0.10",
|
||||||
"expo-status-bar": "57.0.1",
|
"expo-status-bar": "57.0.1",
|
||||||
"expo-symbols": "57.0.3",
|
"expo-symbols": "57.0.3",
|
||||||
|
|||||||
@ -31,7 +31,7 @@ test("the bot's screen opens beside the chat while it works and steps back after
|
|||||||
|
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
await expect(sidePanel).toHaveAttribute("data-auto", "");
|
await expect(sidePanel).toHaveAttribute("data-auto", "");
|
||||||
await expect(page.getByTestId("computer-preview").locator("iframe")).toHaveAttribute(
|
await expect(page.getByTestId("oo-computer-browser-viewport").locator("iframe")).toHaveAttribute(
|
||||||
"src",
|
"src",
|
||||||
SCREEN_URL,
|
SCREEN_URL,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,16 +26,28 @@ test("the bot's cursor glides to where it clicks on its live screen", async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const preview = page.getByTestId("computer-preview");
|
const preview = page.getByTestId("oo-computer-browser-viewport");
|
||||||
await expect(preview.locator("iframe")).toHaveAttribute("src", SCREEN_URL);
|
await expect(preview.locator("iframe")).toHaveAttribute("src", SCREEN_URL);
|
||||||
const cursor = preview.getByTestId("agent-cursor");
|
const cursor = preview.getByTestId("agent-cursor");
|
||||||
await expect(cursor).toHaveCSS("opacity", "0");
|
await expect(cursor).toHaveCSS("opacity", "0");
|
||||||
|
// The click and typing last about a second, so record every action rather than racing a poll.
|
||||||
|
await cursor.evaluate((node) => {
|
||||||
|
const seen: string[] = [];
|
||||||
|
(window as unknown as { cursorActions: string[] }).cursorActions = seen;
|
||||||
|
new MutationObserver(() => seen.push(node.getAttribute("data-action") ?? "")).observe(node, {
|
||||||
|
attributeFilter: ["data-action"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// The scripted runtime clicks the screen center (640, 400) and types.
|
// The scripted runtime clicks the screen center (640, 400) and types.
|
||||||
await rpc(page, "threads/send", { botId, text: "use your screen and type cursor-demo" });
|
await rpc(page, "threads/send", { botId, text: "use your screen and type cursor-demo" });
|
||||||
|
|
||||||
await expect(cursor).toHaveCSS("opacity", "1");
|
await expect(cursor).toHaveCSS("opacity", "1");
|
||||||
await expect(cursor).toHaveAttribute("data-action", /click|type/);
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(() => (window as unknown as { cursorActions: string[] }).cursorActions),
|
||||||
|
)
|
||||||
|
.toEqual(expect.arrayContaining(["click", "type"]));
|
||||||
const layer = (await preview.getByTestId("agent-cursor-layer").boundingBox())!;
|
const layer = (await preview.getByTestId("agent-cursor-layer").boundingBox())!;
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
|
|||||||
@ -12,14 +12,12 @@ test("agent-attached files appear as downloadable cards", async ({ page }, testI
|
|||||||
await composer.fill("write notes/result.txt and attach it to the thread");
|
await composer.fill("write notes/result.txt and attach it to the thread");
|
||||||
await page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
// Scope to ArtifactFileCard text (name + mime); sidebar bot status can also
|
const fileCard = page.getByRole("button", { name: "Preview result.txt" });
|
||||||
// include the prompt path notes/result.txt once status syncs immediately.
|
|
||||||
const fileCard = page.getByRole("button", { name: /result\.txt text\/plain/ });
|
|
||||||
await expect(fileCard).toBeVisible({ timeout: 30_000 });
|
await expect(fileCard).toBeVisible({ timeout: 30_000 });
|
||||||
await captureScreenshot(page, testInfo, "current-file-card");
|
await captureScreenshot(page, testInfo, "current-file-card");
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent("download");
|
const downloadPromise = page.waitForEvent("download");
|
||||||
await fileCard.click();
|
await page.getByRole("button", { name: "Download result.txt" }).click();
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
expect(download.suggestedFilename()).toBe("result.txt");
|
expect(download.suggestedFilename()).toBe("result.txt");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -114,12 +114,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await captureScreenshot(page, testInfo, "27a-bot-settings-model");
|
await captureScreenshot(page, testInfo, "27a-bot-settings-model");
|
||||||
await page.getByRole("button", { name: "Show computer" }).click();
|
await page.getByRole("button", { name: "Show computer" }).click();
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", /^(computer|browser)$/);
|
||||||
await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
|
||||||
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel
|
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel
|
||||||
// chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
|
// chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
|
||||||
const bootOverlay = page.getByText(/Booting up .* computer/);
|
const bootOverlay = page.getByText(/Booting up .* computer/);
|
||||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||||
await expect(bootOverlay).toBeHidden();
|
await expect(bootOverlay).toBeHidden();
|
||||||
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
|
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
|
||||||
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);
|
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);
|
||||||
|
|||||||
@ -39,9 +39,9 @@ test("screen connection failures stay visible and can be retried", async ({ page
|
|||||||
);
|
);
|
||||||
|
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const preview = page.getByTestId("computer-preview");
|
const preview = page.getByTestId("oo-computer-browser-viewport");
|
||||||
await expect(preview.getByRole("alert")).toContainText("temporarily busy");
|
await expect(preview.getByRole("alert")).toContainText("temporarily busy");
|
||||||
await expect(preview.getByTestId("computer-preview-open")).toHaveCount(0);
|
await expect(page.getByTestId("computer-open")).toHaveCount(0);
|
||||||
await captureScreenshot(page, testInfo, "computer-screen-connection-error");
|
await captureScreenshot(page, testInfo, "computer-screen-connection-error");
|
||||||
|
|
||||||
failScreen = false;
|
failScreen = false;
|
||||||
@ -50,14 +50,14 @@ test("screen connection failures stay visible and can be retried", async ({ page
|
|||||||
await expect(preview.getByRole("alert")).toHaveCount(0);
|
await expect(preview.getByRole("alert")).toHaveCount(0);
|
||||||
|
|
||||||
failScreen = true;
|
failScreen = true;
|
||||||
await preview.hover();
|
await page.getByTestId("computer-open").click();
|
||||||
await preview.getByTestId("computer-preview-open").click();
|
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
await expect(page.getByRole("alert")).toContainText("temporarily busy");
|
const fullScreen = page.getByTestId("computer-viewport");
|
||||||
|
await expect(fullScreen.getByRole("alert")).toContainText("temporarily busy");
|
||||||
await captureScreenshot(page, testInfo, "computer-full-screen-connection-error");
|
await captureScreenshot(page, testInfo, "computer-full-screen-connection-error");
|
||||||
|
|
||||||
failScreen = false;
|
failScreen = false;
|
||||||
await page.getByRole("button", { name: "Retry screen" }).click();
|
await fullScreen.getByRole("button", { name: "Retry screen" }).click();
|
||||||
await expect(page.locator('iframe[title="Bot screen"]')).toHaveAttribute("src", screenUrl);
|
await expect(page.locator('iframe[title="Bot screen"]')).toHaveAttribute("src", screenUrl);
|
||||||
await expect(page.getByRole("alert")).toHaveCount(0);
|
await expect(page.getByRole("alert")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -59,9 +59,8 @@ test("computer maintenance shows durable background progress and failure recover
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await expect(page.getByTestId("computer-preview")).toBeVisible();
|
await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||||
await page.getByTestId("computer-preview").hover();
|
await page.getByTestId("computer-open").click();
|
||||||
await page.getByTestId("computer-preview-open").click();
|
|
||||||
await page.getByTestId("computer-more-button").click();
|
await page.getByTestId("computer-more-button").click();
|
||||||
await page.getByRole("menuitem", { name: "Update computer", exact: true }).click();
|
await page.getByRole("menuitem", { name: "Update computer", exact: true }).click();
|
||||||
const dialog = page.getByTestId("computer-update-dialog");
|
const dialog = page.getByTestId("computer-update-dialog");
|
||||||
|
|||||||
@ -67,7 +67,7 @@ test("main conversation does not show a group subtitle for one Oo", async ({ pag
|
|||||||
await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
|
await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung");
|
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung");
|
||||||
await expect(page.getByTestId("oo-group-stage")).not.toContainText("Gruppenunterhaltung");
|
await expect(page.getByTestId("oo-group-stage")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("mobile shell", () => {
|
test.describe("mobile shell", () => {
|
||||||
@ -94,10 +94,11 @@ test("main conversation opens its lead's computer", async ({ page }, testInfo) =
|
|||||||
await signup(page, `main-computer-${stamp}@rakazo.test`, "password12", "Screen");
|
await signup(page, `main-computer-${stamp}@rakazo.test`, "password12", "Screen");
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
await expect(page).toHaveURL(/\/app\/g\//);
|
await expect(page).toHaveURL(/\/app\/g\//);
|
||||||
|
// The lead's computer opens beside the main conversation without leaving it.
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await expect(page).not.toHaveURL(/\/app\/g\//);
|
await expect(page).toHaveURL(/\/app\/g\//);
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "main-conversation-computer");
|
await captureScreenshot(page, testInfo, "main-conversation-computer");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,8 +80,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
|||||||
expect(mainBox).not.toBeNull();
|
expect(mainBox).not.toBeNull();
|
||||||
expect(panelBox).not.toBeNull();
|
expect(panelBox).not.toBeNull();
|
||||||
expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0);
|
expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0);
|
||||||
await sidePanel.getByTestId("computer-preview").hover();
|
await sidePanel.getByTestId("computer-open").click();
|
||||||
await sidePanel.getByTestId("computer-preview-open").click();
|
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Skip", exact: true }).last()).toBeVisible();
|
await expect(page.getByRole("button", { name: "Skip", exact: true }).last()).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "I’m done", exact: true }).last()).toBeVisible();
|
await expect(page.getByRole("button", { name: "I’m done", exact: true }).last()).toBeVisible();
|
||||||
@ -107,8 +106,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
|||||||
}
|
}
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
await sidePanel.getByTestId("computer-preview").hover();
|
const openComputer = sidePanel.getByTestId("computer-open");
|
||||||
const openComputer = sidePanel.getByTestId("computer-preview-open");
|
|
||||||
await expect(openComputer).toBeVisible({ timeout: 30_000 });
|
await expect(openComputer).toBeVisible({ timeout: 30_000 });
|
||||||
await openComputer.click();
|
await openComputer.click();
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
@ -140,6 +138,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
|||||||
);
|
);
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await routinesListed;
|
await routinesListed;
|
||||||
|
await sidePanel.locator('[data-tab="overview"]').click();
|
||||||
await expect(
|
await expect(
|
||||||
sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"),
|
sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@ -316,7 +315,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
|||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
}
|
}
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
const gear = page.getByRole("button", { name: "Show settings" });
|
const gear = page.getByRole("button", { name: "Show settings" }).first();
|
||||||
await gear.click();
|
await gear.click();
|
||||||
const downloadPromise = page.waitForEvent("download");
|
const downloadPromise = page.waitForEvent("download");
|
||||||
await page.getByRole("button", { name: "Export" }).click();
|
await page.getByRole("button", { name: "Export" }).click();
|
||||||
|
|||||||
@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
|||||||
|
|
||||||
const researcherId = await createBot(page, "Researcher");
|
const researcherId = await createBot(page, "Researcher");
|
||||||
const writerId = await createBot(page, "Research Writer");
|
const writerId = await createBot(page, "Research Writer");
|
||||||
|
// Replies read themselves aloud by default; keep that off so only the Speak click below talks.
|
||||||
|
for (const botId of [researcherId, writerId]) {
|
||||||
|
await rpc(page, "bots/update", { botId, autoSpeak: false });
|
||||||
|
}
|
||||||
|
|
||||||
await openNewGroup(page);
|
await openNewGroup(page);
|
||||||
await page.locator("label:has-text('Name') input").fill("Draft team");
|
await page.locator("label:has-text('Name') input").fill("Draft team");
|
||||||
@ -169,18 +173,26 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
|||||||
await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
|
await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
|
||||||
await page.unroute("**/rpc/groups/update");
|
await page.unroute("**/rpc/groups/update");
|
||||||
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
|
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
|
||||||
|
// The panel closes once the save lands; switching chats before that races the layout change.
|
||||||
|
await expect(desktopSettings).toHaveAttribute("data-panel", "closed");
|
||||||
|
|
||||||
await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft");
|
await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft");
|
||||||
await projekte
|
await projekte
|
||||||
.getByRole("button", { name: /^Review team/ })
|
.getByRole("button", { name: /^Review team/ })
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}`));
|
||||||
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
|
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
|
||||||
|
// Each chat keeps its own unsent draft.
|
||||||
await projekte
|
await projekte
|
||||||
.getByRole("button", { name: /^Draft team/ })
|
.getByRole("button", { name: /^Draft team/ })
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
|
await expect(page).toHaveURL(new RegExp(`/app/g/${draftGroupId}`));
|
||||||
|
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(
|
||||||
|
"@Researcher unfinished draft",
|
||||||
|
);
|
||||||
|
await page.getByRole("combobox", { name: "Message" }).fill("");
|
||||||
|
|
||||||
const composer = page.getByRole("combobox", { name: "Message" });
|
const composer = page.getByRole("combobox", { name: "Message" });
|
||||||
await composer.fill("@Res");
|
await composer.fill("@Res");
|
||||||
|
|||||||
@ -61,8 +61,12 @@ export async function createTestRoutine(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openAgentComputerPanel(page: Page) {
|
/** Opens the side panel on the agent's overview, where its routines are listed. */
|
||||||
await page.getByTitle("Agent computer").click();
|
export async function openAgentOverview(page: Page, title = "Agent computer") {
|
||||||
|
const overview = page.getByTestId("side-panel").locator('[data-tab="overview"]');
|
||||||
|
// The header button toggles the panel, so only press it while the panel is closed.
|
||||||
|
if (!(await overview.isVisible())) await page.getByTitle(title).click();
|
||||||
|
await overview.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
|
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts";
|
import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts";
|
||||||
import { captureScreenshot, completeOnboarding, openMainBotThread, rpc, signup } from "./helpers";
|
import {
|
||||||
|
captureScreenshot,
|
||||||
|
completeOnboarding,
|
||||||
|
openAgentOverview,
|
||||||
|
openMainBotThread,
|
||||||
|
rpc,
|
||||||
|
signup,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
/** The nav button reads "Today" or "Today, N need you". */
|
/** The nav button reads "Today" or "Today, N need you". */
|
||||||
function openHeute(page: Page) {
|
function openHeute(page: Page) {
|
||||||
@ -142,7 +149,7 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set
|
|||||||
|
|
||||||
// The main bot's routine list names the check-ins by their key, in the UI language.
|
// The main bot's routine list names the check-ins by their key, in the UI language.
|
||||||
await openMainBotThread(page);
|
await openMainBotThread(page);
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
await expect(page.getByText("Morning brief", { exact: true })).toBeVisible();
|
await expect(page.getByText("Morning brief", { exact: true })).toBeVisible();
|
||||||
await expect(page.getByText("Weekly review", { exact: true })).toBeVisible();
|
await expect(page.getByText("Weekly review", { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,7 @@ test("the in-app browser previews a page and follows it back to the browser", as
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Browser" }).click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const browser = page.getByTestId("in-app-browser");
|
const browser = page.getByTestId("in-app-browser");
|
||||||
await browser.getByTestId("in-app-browser-mode-local").click();
|
await browser.getByTestId("in-app-browser-mode-local").click();
|
||||||
const address = browser.getByRole("textbox", { name: "Address" });
|
const address = browser.getByRole("textbox", { name: "Address" });
|
||||||
@ -39,10 +39,10 @@ test("the in-app browser previews a page and follows it back to the browser", as
|
|||||||
await browser.getByRole("button", { name: "Open in browser" }).click();
|
await browser.getByRole("button", { name: "Open in browser" }).click();
|
||||||
expect((await popup).url()).toBe("https://preview.example/app");
|
expect((await popup).url()).toBe("https://preview.example/app");
|
||||||
|
|
||||||
await browser.getByRole("button", { name: "Close panel" }).click();
|
await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
|
||||||
await expect(browser).toHaveCount(0);
|
await expect(browser).toHaveCount(0);
|
||||||
// The last address comes back when the browser reopens.
|
// The last address comes back when the browser reopens.
|
||||||
await page.getByRole("button", { name: "Browser" }).click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await page.getByTestId("in-app-browser-mode-local").click();
|
await page.getByTestId("in-app-browser-mode-local").click();
|
||||||
await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute(
|
await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute(
|
||||||
"src",
|
"src",
|
||||||
@ -113,7 +113,7 @@ test("on desktop the page is a native view glued to the panel", async ({ page })
|
|||||||
() => (window as unknown as { __browserCalls: Array<[string, unknown?]> }).__browserCalls,
|
() => (window as unknown as { __browserCalls: Array<[string, unknown?]> }).__browserCalls,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Browser" }).click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const browser = page.getByTestId("in-app-browser");
|
const browser = page.getByTestId("in-app-browser");
|
||||||
await browser.getByTestId("in-app-browser-mode-local").click();
|
await browser.getByTestId("in-app-browser-mode-local").click();
|
||||||
await browser.getByRole("textbox", { name: "Address" }).fill("localhost:5173");
|
await browser.getByRole("textbox", { name: "Address" }).fill("localhost:5173");
|
||||||
@ -157,6 +157,6 @@ test("on desktop the page is a native view glued to the panel", async ({ page })
|
|||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show");
|
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show");
|
||||||
|
|
||||||
await browser.getByRole("button", { name: "Close panel" }).click();
|
await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
|
||||||
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide");
|
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,7 +26,8 @@ test("onboarding lands in the main conversation, its lead answers, and addressed
|
|||||||
await expect(mainEntry).toBeVisible();
|
await expect(mainEntry).toBeVisible();
|
||||||
// The main bot is reached through the main conversation, not listed again.
|
// The main bot is reached through the main conversation, not listed again.
|
||||||
await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0);
|
await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0);
|
||||||
await expect(page.getByTestId("oo-group-stage")).toBeVisible();
|
await expect(page.getByTestId("oo-group-stage")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("bot-settings-trigger")).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "main-conversation-landing");
|
await captureScreenshot(page, testInfo, "main-conversation-landing");
|
||||||
|
|
||||||
const snapshot = () => rpc<Snapshot>(page, "threads/get", { groupId });
|
const snapshot = () => rpc<Snapshot>(page, "threads/get", { groupId });
|
||||||
|
|||||||
@ -73,7 +73,7 @@ test("an image in the main conversation opens and downloads from the artifact ro
|
|||||||
await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
|
await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
|
||||||
await thumbnail.click();
|
await thumbnail.click();
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog", { name: "pixel.png" });
|
const dialog = page.getByRole("dialog", { name: "Pixel" });
|
||||||
const download = dialog.getByRole("link", { name: "Download pixel.png" });
|
const download = dialog.getByRole("link", { name: "Download pixel.png" });
|
||||||
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
|
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
|
||||||
const saved = page.waitForEvent("download");
|
const saved = page.waitForEvent("download");
|
||||||
|
|||||||
@ -92,7 +92,13 @@ test("message hover shows below-bubble actions; reply links to parent", async ({
|
|||||||
);
|
);
|
||||||
const botText = page.getByText(/i handled: hover-bot-/);
|
const botText = page.getByText(/i handled: hover-bot-/);
|
||||||
await expect(transcript.getByText(/i handled: hover-bot-/)).toBeVisible({ timeout: 20_000 });
|
await expect(transcript.getByText(/i handled: hover-bot-/)).toBeVisible({ timeout: 20_000 });
|
||||||
const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first();
|
// Pin the row by id: once the run settles, long replies fold their details away.
|
||||||
|
const botMessageId = await transcript
|
||||||
|
.locator(`[data-message-id]`)
|
||||||
|
.filter({ has: botText })
|
||||||
|
.first()
|
||||||
|
.getAttribute("data-message-id");
|
||||||
|
const botRow = transcript.locator(`[data-message-id="${botMessageId}"]`);
|
||||||
await expect(botRow).toBeVisible();
|
await expect(botRow).toBeVisible();
|
||||||
await expectRailAtRest(page, botRow);
|
await expectRailAtRest(page, botRow);
|
||||||
await captureScreenshot(page, testInfo, "message-actions-rest-desktop");
|
await captureScreenshot(page, testInfo, "message-actions-rest-desktop");
|
||||||
|
|||||||
@ -69,7 +69,7 @@ test("Oo browser shows live screen, cursor, cowork click, and user context", asy
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Browser", exact: true }).click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const browser = page.getByTestId("in-app-browser");
|
const browser = page.getByTestId("in-app-browser");
|
||||||
await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute(
|
await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute(
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@ -18,7 +18,8 @@ test("live page helper preserves identity, masks passwords, and reports partial
|
|||||||
res.setHeader("content-type", "text/html");
|
res.setHeader("content-type", "text/html");
|
||||||
res.end(`<!doctype html><title>Fixture</title><p>Readable article content</p>
|
res.end(`<!doctype html><title>Fixture</title><p>Readable article content</p>
|
||||||
<label>Name <input id="name"></label><input aria-label="Password" type="password" value="fake-test-password">
|
<label>Name <input id="name"></label><input aria-label="Password" type="password" value="fake-test-password">
|
||||||
<button id="save" onclick="this.dataset.clicks=Number(this.dataset.clicks||0)+1">Save</button>
|
<select id="plan" aria-label="Plan"><option value="free">Free</option><option value="pro">Pro Plan</option></select>
|
||||||
|
<button id="save" onmouseenter="this.dataset.hovered='1'" onclick="this.dataset.clicks=Number(this.dataset.clicks||0)+1">Save</button>
|
||||||
<label><input type="checkbox">Agree</label><button disabled>Disabled</button>
|
<label><input type="checkbox">Agree</label><button disabled>Disabled</button>
|
||||||
<script>window.__rakazoPageBrowser={snapshot:()=>({title:'spoofed'})};</script>`);
|
<script>window.__rakazoPageBrowser={snapshot:()=>({title:'spoofed'})};</script>`);
|
||||||
});
|
});
|
||||||
@ -83,9 +84,18 @@ test("live page helper preserves identity, masks passwords, and reports partial
|
|||||||
const refreshed = await command("snapshot");
|
const refreshed = await command("snapshot");
|
||||||
expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save"));
|
expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save"));
|
||||||
expect(
|
expect(
|
||||||
await command("act", { actions: [{ kind: "click", ref: ref(refreshed, "Agree") }] }),
|
await command("act", {
|
||||||
).toMatchObject({ ok: true, completed: 1 });
|
actions: [
|
||||||
|
{ kind: "click", ref: ref(refreshed, "Agree") },
|
||||||
|
{ kind: "select", ref: ref(refreshed, "Plan"), text: "Pro Plan" },
|
||||||
|
{ kind: "press", ref: ref(refreshed, "Name"), key: "Enter" },
|
||||||
|
{ kind: "scroll", direction: "down", amount: 2 },
|
||||||
|
{ kind: "wait", ms: 10 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toMatchObject({ ok: true, completed: 5 });
|
||||||
await expect(page.getByRole("checkbox")).toBeChecked();
|
await expect(page.getByRole("checkbox")).toBeChecked();
|
||||||
|
await expect(page.locator("#plan")).toHaveValue("pro");
|
||||||
expect(
|
expect(
|
||||||
await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }),
|
await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }),
|
||||||
).toMatchObject({ ok: true, title: "Fixture" });
|
).toMatchObject({ ok: true, title: "Fixture" });
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
completeOnboarding,
|
completeOnboarding,
|
||||||
createNamedBot,
|
createNamedBot,
|
||||||
createTestRoutine,
|
createTestRoutine,
|
||||||
openAgentComputerPanel,
|
openAgentOverview,
|
||||||
rpc,
|
rpc,
|
||||||
signup,
|
signup,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@ -33,7 +33,7 @@ test("routine active switch keeps its thumb inside the track", async ({ page },
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
const botId = activeBotId(page);
|
const botId = activeBotId(page);
|
||||||
await createTestRoutine(page, botId, { name: "Toggle styling" });
|
await createTestRoutine(page, botId, { name: "Toggle styling" });
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
await page.getByRole("button", { name: /Toggle styling/ }).click();
|
await page.getByRole("button", { name: /Toggle styling/ }).click();
|
||||||
|
|
||||||
const toggle = page.getByRole("switch", { name: "Active" });
|
const toggle = page.getByRole("switch", { name: "Active" });
|
||||||
@ -89,7 +89,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist
|
|||||||
expect(created.nextRunAt).not.toBeNull();
|
expect(created.nextRunAt).not.toBeNull();
|
||||||
expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 });
|
expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 });
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
|
|
||||||
await page.getByRole("button", { name: /Tokyo check-in/ }).click();
|
await page.getByRole("button", { name: /Tokyo check-in/ }).click();
|
||||||
await page.locator("label:has-text('Name') input").fill("Weekday check-in");
|
await page.locator("label:has-text('Name') input").fill("Weekday check-in");
|
||||||
@ -133,7 +133,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist
|
|||||||
expect(await rpc<Routine[]>(page, "routines/list", { botId })).toEqual([]);
|
expect(await rpc<Routine[]>(page, "routines/list", { botId })).toEqual([]);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
await expect(updatedButton).toHaveCount(0);
|
await expect(updatedButton).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ test("a successful routine create is not reported as failed when refresh fails",
|
|||||||
name: "Persisted routine",
|
name: "Persisted routine",
|
||||||
prompt: "Run once each morning",
|
prompt: "Run once each morning",
|
||||||
});
|
});
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
await page.getByRole("button", { name: /Persisted routine/ }).click();
|
await page.getByRole("button", { name: /Persisted routine/ }).click();
|
||||||
await page
|
await page
|
||||||
.locator("label:has-text('Instruction') textarea")
|
.locator("label:has-text('Instruction') textarea")
|
||||||
@ -240,7 +240,7 @@ test("switching bots while a routine save is pending does not reopen stale state
|
|||||||
]);
|
]);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
await page.getByRole("button", { name: /First routine/ }).click();
|
await page.getByRole("button", { name: /First routine/ }).click();
|
||||||
await page.locator("label:has-text('Name') input").fill("First routine updated");
|
await page.locator("label:has-text('Name') input").fill("First routine updated");
|
||||||
|
|
||||||
@ -280,7 +280,7 @@ test("switching bots while a routine save is pending does not reopen stale state
|
|||||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
||||||
await expect(page.getByRole("alert")).toHaveCount(0);
|
await expect(page.getByRole("alert")).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
await expect(page.getByRole("button", { name: /Second routine/ })).toHaveCount(1);
|
await expect(page.getByRole("button", { name: /Second routine/ })).toHaveCount(1);
|
||||||
await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0);
|
await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
captureScreenshot,
|
captureScreenshot,
|
||||||
completeOnboarding,
|
completeOnboarding,
|
||||||
createTestRoutine,
|
createTestRoutine,
|
||||||
openAgentComputerPanel,
|
openAgentOverview,
|
||||||
rpc,
|
rpc,
|
||||||
signup,
|
signup,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@ -38,7 +38,7 @@ test("Slack message trigger uses the mounted messaging provider and persists", a
|
|||||||
githubEnabled: false,
|
githubEnabled: false,
|
||||||
messageProvider: "slack",
|
messageProvider: "slack",
|
||||||
});
|
});
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
const panel = page.getByTestId("side-panel");
|
const panel = page.getByTestId("side-panel");
|
||||||
await expect(panel.getByText("Slack message", { exact: true })).toBeVisible();
|
await expect(panel.getByText("Slack message", { exact: true })).toBeVisible();
|
||||||
const [routine] = await rpc<Routine[]>(page, "routines/list", { botId });
|
const [routine] = await rpc<Routine[]>(page, "routines/list", { botId });
|
||||||
@ -72,7 +72,7 @@ test("GitHub event trigger exposes signed delivery settings and persists", async
|
|||||||
githubEnabled: true,
|
githubEnabled: true,
|
||||||
messageProvider: null,
|
messageProvider: null,
|
||||||
});
|
});
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
await page.getByRole("button", { name: /Review repository events/ }).click();
|
await page.getByRole("button", { name: /Review repository events/ }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("side-panel").getByText("Git event", { exact: true }),
|
page.getByTestId("side-panel").getByText("Git event", { exact: true }),
|
||||||
@ -111,7 +111,7 @@ test("Korean webhook routine keeps technical field labels in English", async ({
|
|||||||
crons: [],
|
crons: [],
|
||||||
webhookEnabled: true,
|
webhookEnabled: true,
|
||||||
});
|
});
|
||||||
await page.getByTitle("Agent 컴퓨터").click();
|
await openAgentOverview(page, "Agent 컴퓨터");
|
||||||
await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click();
|
await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click();
|
||||||
|
|
||||||
await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible();
|
await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible();
|
||||||
@ -127,7 +127,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
const botId = activeBotId(page);
|
const botId = activeBotId(page);
|
||||||
|
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0);
|
await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0);
|
||||||
await createTestRoutine(page, botId, {
|
await createTestRoutine(page, botId, {
|
||||||
name: "Daily verification",
|
name: "Daily verification",
|
||||||
@ -135,7 +135,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
|||||||
crons: ["0 9 * * 1-5"],
|
crons: ["0 9 * * 1-5"],
|
||||||
});
|
});
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
const routine = page.getByRole("button", { name: /Daily verification/ });
|
const routine = page.getByRole("button", { name: /Daily verification/ });
|
||||||
await expect(routine).toContainText("Weekdays at 9:00 AM");
|
await expect(routine).toContainText("Weekdays at 9:00 AM");
|
||||||
await captureScreenshot(page, testInfo, "33-routine-scheduled");
|
await captureScreenshot(page, testInfo, "33-routine-scheduled");
|
||||||
@ -148,7 +148,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
|||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible();
|
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible();
|
||||||
await page.getByTitle("Agent computer").click();
|
await openAgentOverview(page);
|
||||||
await expect(page.getByRole("button", { name: /Daily verification/ })).toContainText(
|
await expect(page.getByRole("button", { name: /Daily verification/ })).toContainText(
|
||||||
"Weekdays at 9:00 AM",
|
"Weekdays at 9:00 AM",
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
activeBotId,
|
activeBotId,
|
||||||
completeOnboarding,
|
completeOnboarding,
|
||||||
createTestRoutine,
|
createTestRoutine,
|
||||||
openAgentComputerPanel,
|
openAgentOverview,
|
||||||
rpc,
|
rpc,
|
||||||
signup,
|
signup,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@ -17,14 +17,14 @@ test("routines section is hidden until the Oo has one, then list toggle persists
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
const botId = activeBotId(page);
|
const botId = activeBotId(page);
|
||||||
|
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
const panel = page.getByTestId("side-panel");
|
const panel = page.getByTestId("side-panel");
|
||||||
await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0);
|
await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0);
|
||||||
await expect(page.getByTestId("routine-create-button")).toHaveCount(0);
|
await expect(page.getByTestId("routine-create-button")).toHaveCount(0);
|
||||||
|
|
||||||
await createTestRoutine(page, botId, { name: "Morning check-in" });
|
await createTestRoutine(page, botId, { name: "Morning check-in" });
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await openAgentComputerPanel(page);
|
await openAgentOverview(page);
|
||||||
await expect(panel.getByText("Routines", { exact: true })).toBeVisible();
|
await expect(panel.getByText("Routines", { exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible();
|
await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@ -16,18 +16,16 @@ test("teach a task records interaction and saves a draft", async ({ page }, test
|
|||||||
await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0);
|
await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0);
|
||||||
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
|
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
|
||||||
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
|
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
|
||||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||||
// Showing the panel never boots the computer; its Open action is visible without hover.
|
// Showing the panel never boots the computer; its Open action is visible without hover.
|
||||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
const openButton = sidePanel.getByTestId("computer-open");
|
||||||
await expect(openButton).toBeVisible();
|
await expect(openButton).toBeVisible();
|
||||||
const botId = activeBotId(page);
|
const botId = activeBotId(page);
|
||||||
expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped");
|
expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped");
|
||||||
await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
|
await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
|
||||||
|
|
||||||
const preview = sidePanel.getByTestId("computer-preview");
|
const preview = sidePanel.getByTestId("oo-computer-browser-viewport");
|
||||||
await preview.hover();
|
|
||||||
await expect(openButton).toBeVisible();
|
await expect(openButton).toBeVisible();
|
||||||
await expect(openButton.getByText("Open", { exact: true })).toBeVisible();
|
|
||||||
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");
|
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");
|
||||||
|
|
||||||
await openButton.click();
|
await openButton.click();
|
||||||
|
|||||||
@ -10,7 +10,7 @@ test("blocks the composer while a skill is being taught", async ({ page }) => {
|
|||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
const openButton = sidePanel.getByTestId("computer-open");
|
||||||
await expect(openButton).toBeVisible();
|
await expect(openButton).toBeVisible();
|
||||||
await openButton.click();
|
await openButton.click();
|
||||||
const chrome = page.getByTestId("computer-chrome");
|
const chrome = page.getByTestId("computer-chrome");
|
||||||
|
|||||||
@ -51,7 +51,7 @@ test("Team Computer gives bots a home folder plus shared space while Private sta
|
|||||||
|
|
||||||
const privateId = await createBot(page, "Private Writer", "dedicated");
|
const privateId = await createBot(page, "Private Writer", "dedicated");
|
||||||
await openComputerPanel(page);
|
await openComputerPanel(page);
|
||||||
await expect(page.getByText(/Private Writer['’]s screen/).last()).toBeVisible();
|
await expect(page.getByText(/Private Writer['’]s browser/).last()).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "43-private-computer");
|
await captureScreenshot(page, testInfo, "43-private-computer");
|
||||||
await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({
|
await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({
|
||||||
ok: false,
|
ok: false,
|
||||||
@ -94,8 +94,7 @@ test("user control leaves another Team bot's screen available", async ({ page },
|
|||||||
// The sidebar entry for the main bot opens the main conversation; its computer is in the 1:1.
|
// The sidebar entry for the main bot opens the main conversation; its computer is in the 1:1.
|
||||||
await openMainBotThread(page);
|
await openMainBotThread(page);
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await page.getByTestId("computer-preview").hover();
|
await page.getByTestId("computer-open").click();
|
||||||
await page.getByTestId("computer-preview-open").click();
|
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Close computer" }).click();
|
await page.getByRole("button", { name: "Close computer" }).click();
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0);
|
await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0);
|
||||||
@ -132,8 +131,7 @@ test("a failed control release keeps the computer open for retry", async ({ page
|
|||||||
await signup(page, `team-release-${Date.now()}@rakazo.test`, "password12", "Team Release");
|
await signup(page, `team-release-${Date.now()}@rakazo.test`, "password12", "Team Release");
|
||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await page.getByTestId("computer-preview").hover();
|
await page.getByTestId("computer-open").click();
|
||||||
await page.getByTestId("computer-preview-open").click();
|
|
||||||
const chrome = page.getByTestId("computer-chrome");
|
const chrome = page.getByTestId("computer-chrome");
|
||||||
const release = chrome.getByRole("button", { name: "Release", exact: true });
|
const release = chrome.getByRole("button", { name: "Release", exact: true });
|
||||||
await expect(release).toBeVisible();
|
await expect(release).toBeVisible();
|
||||||
@ -192,10 +190,8 @@ test("an active Team bot must be stopped before user takeover", async ({ page },
|
|||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
||||||
await page.getByTestId("computer-preview").hover();
|
const openBusy = sidePanel.getByTestId("computer-open");
|
||||||
const openBusy = sidePanel.getByTestId("computer-preview-open");
|
|
||||||
await expect(openBusy).toBeVisible();
|
await expect(openBusy).toBeVisible();
|
||||||
await expect(openBusy.getByText("Open", { exact: true })).toBeVisible();
|
|
||||||
await openBusy.click();
|
await openBusy.click();
|
||||||
const chrome = page.getByTestId("computer-chrome");
|
const chrome = page.getByTestId("computer-chrome");
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
@ -221,8 +217,7 @@ test("an active Team bot must be stopped before user takeover", async ({ page },
|
|||||||
.toBeNull();
|
.toBeNull();
|
||||||
|
|
||||||
// After stop, Open via hover is the takeover path (no Take control button).
|
// After stop, Open via hover is the takeover path (no Take control button).
|
||||||
await page.getByTestId("computer-preview").hover();
|
await page.getByTestId("computer-open").click();
|
||||||
await page.getByTestId("computer-preview-open").click();
|
|
||||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||||
await expect(chrome.getByText("You have control", { exact: true })).toBeVisible();
|
await expect(chrome.getByText("You have control", { exact: true })).toBeVisible();
|
||||||
await expect(chrome.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
await expect(chrome.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
||||||
@ -287,8 +282,8 @@ async function expectComposerReady(page: Page) {
|
|||||||
|
|
||||||
async function openComputerPanel(page: Page) {
|
async function openComputerPanel(page: Page) {
|
||||||
await page.getByTitle("Agent computer").click();
|
await page.getByTitle("Agent computer").click();
|
||||||
await expect(page.getByTestId("computer-preview")).toBeVisible();
|
await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||||
await expect(page.getByTestId("computer-preview-open")).toHaveCount(1);
|
await expect(page.getByTestId("computer-open")).toHaveCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendAndWait(page: Page, botId: string, text: string) {
|
async function sendAndWait(page: Page, botId: string, text: string) {
|
||||||
|
|||||||
@ -14,6 +14,13 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
|
|||||||
await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible();
|
await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible();
|
||||||
await expect(projekte.getByText("Projects", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Projects", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
||||||
|
const [projekteBox, unterhaltungenBox] = await Promise.all([
|
||||||
|
projekte.boundingBox(),
|
||||||
|
unterhaltungen.boundingBox(),
|
||||||
|
]);
|
||||||
|
expect(projekteBox).not.toBeNull();
|
||||||
|
expect(unterhaltungenBox).not.toBeNull();
|
||||||
|
expect(projekteBox!.y).toBeLessThan(unterhaltungenBox!.y);
|
||||||
|
|
||||||
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||||
const picker = page.getByTestId("conversation-bot-picker");
|
const picker = page.getByTestId("conversation-bot-picker");
|
||||||
@ -40,14 +47,14 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
|
|||||||
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0);
|
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
// The main agent leads the picker, even after other bots exist.
|
// The new project's main agent leads the picker, even after other bots exist.
|
||||||
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByTestId("conversation-bot-picker")
|
.getByTestId("conversation-bot-picker")
|
||||||
.locator("[data-testid^='conversation-picker-bot-']")
|
.locator("[data-testid^='conversation-picker-bot-']")
|
||||||
.first(),
|
.first(),
|
||||||
).toHaveText("Chief");
|
).toHaveText("Projektmanager");
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
// Creating a project that already exists opens it instead of listing it twice.
|
// Creating a project that already exists opens it instead of listing it twice.
|
||||||
|
|||||||
@ -7,7 +7,15 @@ async function turnOnSpeaking(page: Page) {
|
|||||||
await expect(voice).toBeVisible();
|
await expect(voice).toBeVisible();
|
||||||
await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0);
|
await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0);
|
||||||
await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0);
|
await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0);
|
||||||
await voice.getByRole("switch", { name: "Speaking on or off" }).click();
|
// The app turns the local voice on by itself the first time it starts, and the switch stays
|
||||||
|
// disabled while that runs; only click once it settled off.
|
||||||
|
const speaking = voice.getByRole("switch", { name: "Speaking on or off" });
|
||||||
|
await expect(async () => {
|
||||||
|
if ((await speaking.getAttribute("aria-checked")) !== "true" && (await speaking.isEnabled())) {
|
||||||
|
await speaking.click();
|
||||||
|
}
|
||||||
|
await expect(speaking).toHaveAttribute("aria-checked", "true", { timeout: 3_000 });
|
||||||
|
}).toPass({ timeout: 30_000 });
|
||||||
await expect(voice.getByTestId("voice-carousel")).toBeVisible();
|
await expect(voice.getByTestId("voice-carousel")).toBeVisible();
|
||||||
return voice;
|
return voice;
|
||||||
}
|
}
|
||||||
@ -25,11 +33,6 @@ test("voice settings pick a voice from the carousel, speak a reply, and open a c
|
|||||||
).toHaveCount(1);
|
).toHaveCount(1);
|
||||||
await captureScreenshot(page, testInfo, "voice-composer");
|
await captureScreenshot(page, testInfo, "voice-composer");
|
||||||
|
|
||||||
const preparedOff = await rpc<{ ready: boolean }>(page, "voice/prepare", {
|
|
||||||
text: "Hello there.",
|
|
||||||
});
|
|
||||||
expect(preparedOff.ready).toBe(false);
|
|
||||||
|
|
||||||
const voice = await turnOnSpeaking(page);
|
const voice = await turnOnSpeaking(page);
|
||||||
const cards = voice.getByTestId("voice-card");
|
const cards = voice.getByTestId("voice-card");
|
||||||
// English UI lists only the English voices of the local server.
|
// English UI lists only the English voices of the local server.
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta name="description" content="Deine Oos — immer da, immer wach." />
|
<meta name="description" content="Deine Oos — immer da, immer wach." />
|
||||||
<meta name="theme-color" content="#07080a" />
|
<meta name="theme-color" content="#07080a" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="evOo" />
|
<meta name="apple-mobile-web-app-title" content="evOo" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"id": "/",
|
||||||
"name": "evOo",
|
"name": "evOo",
|
||||||
"short_name": "evOo",
|
"short_name": "evOo",
|
||||||
"description": "Deine Oos — immer da, immer wach.",
|
"description": "Deine Oos — immer da, immer wach.",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0f1218",
|
"orientation": "any",
|
||||||
"theme_color": "#0f1218",
|
"background_color": "#07080a",
|
||||||
|
"theme_color": "#07080a",
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ self.addEventListener("push", (event) => {
|
|||||||
// Same tag as the in-app notification, so one replaces the other.
|
// Same tag as the in-app notification, so one replaces the other.
|
||||||
tag: typeof data.tag === "string" && data.tag ? data.tag : undefined,
|
tag: typeof data.tag === "string" && data.tag ? data.tag : undefined,
|
||||||
icon: "/icon-192.png",
|
icon: "/icon-192.png",
|
||||||
|
badge: "/icon-192.png",
|
||||||
data: { url: appPath(data.url) },
|
data: { url: appPath(data.url) },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { ChatMarkdown } from "@rakazo/chat-ui/web";
|
import { ChatMarkdown } from "@rakazo/chat-ui/web";
|
||||||
|
import { isPaneArtifactMimeType } from "@rakazo/core";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
|
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
|
||||||
import { Download, FileText, X } from "lucide-react";
|
import { Download, Eye, FileText, X } from "lucide-react";
|
||||||
import { type RefObject, useMemo, useRef, useState } from "react";
|
import type { RefObject } from "react";
|
||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { useArtifactBytes } from "../hooks/useArtifactBytes";
|
import { useArtifactBytes } from "../hooks/useArtifactBytes";
|
||||||
import { downloadArtifact } from "../lib/artifact-open";
|
import { downloadArtifact } from "../lib/artifact-open";
|
||||||
import { formatBytes } from "../lib/format-bytes";
|
import { formatBytes } from "../lib/format-bytes";
|
||||||
@ -16,63 +18,85 @@ type ArtifactFileCardProps = {
|
|||||||
onOpenBeside?: () => void;
|
onOpenBeside?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INLINE_DIALOG_TEXT_MIME_TYPES = new Set(["text/markdown", "text/plain", "application/json"]);
|
||||||
|
|
||||||
|
function shortFileTypeLabel(name: string, mimeType: string): string {
|
||||||
|
const dot = name.lastIndexOf(".");
|
||||||
|
if (dot > 0 && dot < name.length - 1) {
|
||||||
|
const ext = name
|
||||||
|
.slice(dot + 1)
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
if (ext.length <= 6) return ext;
|
||||||
|
}
|
||||||
|
const sub = mimeType.split("/")[1]?.split("+")[0]?.toUpperCase();
|
||||||
|
return sub && sub.length <= 8 ? sub : "FILE";
|
||||||
|
}
|
||||||
|
|
||||||
export function ArtifactFileCard(props: ArtifactFileCardProps) {
|
export function ArtifactFileCard(props: ArtifactFileCardProps) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const markdown = props.mimeType === "text/markdown";
|
const canOpenBeside = Boolean(props.onOpenBeside) && isPaneArtifactMimeType(props.mimeType);
|
||||||
|
const canOpenDialog = INLINE_DIALOG_TEXT_MIME_TYPES.has(props.mimeType);
|
||||||
|
const canPreview = canOpenBeside || canOpenDialog;
|
||||||
const previewButton = useRef<HTMLButtonElement>(null);
|
const previewButton = useRef<HTMLButtonElement>(null);
|
||||||
const closeButton = useRef<HTMLButtonElement>(null);
|
const closeButton = useRef<HTMLButtonElement>(null);
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const startDownload = () => downloadArtifact(props.artifactId, props.name);
|
const startDownload = () => downloadArtifact(props.artifactId, props.name);
|
||||||
|
const badgeLabel = shortFileTypeLabel(props.name, props.mimeType);
|
||||||
if (!markdown) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={startDownload}
|
|
||||||
className="rounded-2xl border border-border bg-card px-4 py-3 text-left text-ui-body text-foreground hover:bg-accent"
|
|
||||||
>
|
|
||||||
<div className="font-medium">{props.name}</div>
|
|
||||||
<div className="mt-1 text-muted-foreground">
|
|
||||||
{props.mimeType} · {formatBytes(props.size)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div data-testid="artifact-file-card">
|
||||||
<div className="flex min-w-[280px] overflow-hidden rounded-2xl border border-border bg-card text-left text-foreground">
|
<div className="flex min-w-[280px] max-w-md overflow-hidden rounded-2xl border border-border bg-card text-left text-foreground shadow-2xs transition-colors">
|
||||||
<button
|
<button
|
||||||
ref={previewButton}
|
ref={previewButton}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Preview ${props.name}`}
|
aria-label={canPreview ? t`Preview ${props.name}` : t`Download ${props.name}`}
|
||||||
onClick={() => (props.onOpenBeside ? props.onOpenBeside() : setPreviewOpen(true))}
|
onClick={() => {
|
||||||
className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3 text-left hover:bg-accent"
|
if (canOpenBeside && props.onOpenBeside) {
|
||||||
|
props.onOpenBeside();
|
||||||
|
} else if (canOpenDialog) {
|
||||||
|
setPreviewOpen(true);
|
||||||
|
} else {
|
||||||
|
startDownload();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent"
|
||||||
>
|
>
|
||||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-[10px] bg-muted text-foreground">
|
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-[10px] bg-muted text-foreground">
|
||||||
<FileText size={21} strokeWidth={1.8} />
|
<FileText size={20} strokeWidth={1.8} />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-ui-body font-medium">{props.name}</span>
|
<span className="block truncate text-ui-body font-medium">{props.name}</span>
|
||||||
<span className="mt-0.5 block text-ui-body-sm text-muted-foreground">
|
<span className="mt-0.5 flex items-center gap-1.5 text-ui-body-sm text-muted-foreground">
|
||||||
{formatBytes(props.size)}
|
<span className="rounded bg-muted px-1.5 py-0.2 font-mono text-ui-label text-foreground/80">
|
||||||
|
{badgeLabel}
|
||||||
|
</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{formatBytes(props.size)}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
{canPreview ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="shrink-0 text-muted-foreground/70 transition-colors"
|
||||||
|
>
|
||||||
|
<Eye size={16} strokeWidth={1.8} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Download ${props.name}`}
|
aria-label={t`Download ${props.name}`}
|
||||||
title={t`Download ${props.name}`}
|
title={t`Download ${props.name}`}
|
||||||
onClick={startDownload}
|
onClick={startDownload}
|
||||||
className="grid w-14 shrink-0 place-items-center border-l border-border text-muted-foreground hover:bg-accent hover:text-foreground"
|
className="grid w-13 shrink-0 place-items-center border-l border-border text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Download strokeWidth={1.8} className="size-icon-lg" />
|
<Download strokeWidth={1.8} className="size-icon-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{canOpenDialog ? (
|
||||||
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
@ -80,16 +104,18 @@ export function ArtifactFileCard(props: ArtifactFileCardProps) {
|
|||||||
finalFocus={previewButton}
|
finalFocus={previewButton}
|
||||||
className="flex h-[min(88vh,900px)] w-[min(960px,94vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-none"
|
className="flex h-[min(88vh,900px)] w-[min(960px,94vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-none"
|
||||||
>
|
>
|
||||||
<MarkdownPreview {...props} closeButtonRef={closeButton} />
|
<TextFilePreview {...props} closeButtonRef={closeButton} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkdownPreview({
|
function TextFilePreview({
|
||||||
artifactId,
|
artifactId,
|
||||||
name,
|
name,
|
||||||
|
mimeType,
|
||||||
closeButtonRef,
|
closeButtonRef,
|
||||||
}: ArtifactFileCardProps & { closeButtonRef: RefObject<HTMLButtonElement | null> }) {
|
}: ArtifactFileCardProps & { closeButtonRef: RefObject<HTMLButtonElement | null> }) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@ -97,8 +123,8 @@ function MarkdownPreview({
|
|||||||
const state = useMemo(() => {
|
const state = useMemo(() => {
|
||||||
if (loaded.status !== "ready") return loaded;
|
if (loaded.status !== "ready") return loaded;
|
||||||
try {
|
try {
|
||||||
const markdown = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes);
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes);
|
||||||
return { status: "ready" as const, markdown };
|
return { status: "ready" as const, text };
|
||||||
} catch {
|
} catch {
|
||||||
return { status: "error" as const, message: t`This file is not valid UTF-8 Markdown.` };
|
return { status: "error" as const, message: t`This file is not valid UTF-8 Markdown.` };
|
||||||
}
|
}
|
||||||
@ -140,8 +166,12 @@ function MarkdownPreview({
|
|||||||
<div className="rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3 text-destructive">
|
<div className="rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3 text-destructive">
|
||||||
{state.message}
|
{state.message}
|
||||||
</div>
|
</div>
|
||||||
|
) : mimeType === "text/markdown" ? (
|
||||||
|
<ChatMarkdown>{state.text}</ChatMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<ChatMarkdown>{state.markdown}</ChatMarkdown>
|
<pre className="whitespace-pre-wrap break-words font-mono text-ui-body-sm leading-6 text-foreground">
|
||||||
|
{state.text}
|
||||||
|
</pre>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,17 +1,115 @@
|
|||||||
import { selectedAskActionLabel } from "@rakazo/core";
|
// @vitest-environment jsdom
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("selectedAskActionLabel", () => {
|
import type { ReactNode } from "react";
|
||||||
it("maps a choice answer id to its user-facing label", () => {
|
import { act } from "react";
|
||||||
expect(
|
import type { Root } from "react-dom/client";
|
||||||
selectedAskActionLabel("choice-2", [
|
import { createRoot } from "react-dom/client";
|
||||||
{ id: "choice-1", label: "Berlin" },
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||||
{ id: "choice-2", label: "Seoul" },
|
|
||||||
]),
|
|
||||||
).toBe("Seoul");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to the answer when an action is unavailable", () => {
|
vi.mock("@lingui/core/macro", () => ({
|
||||||
expect(selectedAskActionLabel("custom", undefined)).toBe("custom");
|
t: (parts: TemplateStringsArray, ...values: unknown[]) =>
|
||||||
});
|
parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), ""),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@lingui/react/macro", () => {
|
||||||
|
const t = (parts: TemplateStringsArray, ...values: unknown[]) =>
|
||||||
|
parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), "");
|
||||||
|
return { useLingui: () => ({ t }), Trans: ({ children }: { children: ReactNode }) => children };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@rakazo/chat-ui/web", () => ({
|
||||||
|
ChatMarkdown: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import type { AskBlock } from "./AskCard";
|
||||||
|
import { AskCard } from "./AskCard";
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders primary approval button text with text-primary-foreground for contrast", async () => {
|
||||||
|
const onAnswer = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const block: AskBlock = {
|
||||||
|
kind: "ask",
|
||||||
|
text: "**Review before shell**\n\nThis run would push commits to a remote.",
|
||||||
|
detail: "git push",
|
||||||
|
status: "pending",
|
||||||
|
approvalEffectId: "effect-1",
|
||||||
|
actions: [
|
||||||
|
{ id: "allow", label: "Allow once" },
|
||||||
|
{ id: "always", label: "Always allow this tool" },
|
||||||
|
{ id: "deny", label: "Deny" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<AskCard block={block} canAnswer={true} onAnswer={onAnswer} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = Array.from(container.querySelectorAll("button"));
|
||||||
|
expect(buttons).toHaveLength(3);
|
||||||
|
|
||||||
|
const [allowButton, alwaysButton, denyButton] = buttons;
|
||||||
|
const allowSpan = allowButton?.querySelector("span");
|
||||||
|
const alwaysSpan = alwaysButton?.querySelector("span");
|
||||||
|
const denySpan = denyButton?.querySelector("span");
|
||||||
|
const allowIcon = allowButton?.querySelector("svg");
|
||||||
|
const alwaysIcon = alwaysButton?.querySelector("svg");
|
||||||
|
|
||||||
|
expect(allowSpan?.textContent).toBe("Allow once");
|
||||||
|
expect(allowSpan?.className).toContain("text-primary-foreground");
|
||||||
|
expect(allowSpan?.className).not.toContain("text-foreground");
|
||||||
|
expect(allowIcon?.getAttribute("class")).toContain("text-primary-foreground/75");
|
||||||
|
|
||||||
|
expect(alwaysSpan?.textContent).toBe("Always allow this tool");
|
||||||
|
expect(alwaysSpan?.className).toContain("text-foreground");
|
||||||
|
expect(alwaysSpan?.className).not.toContain("text-primary-foreground");
|
||||||
|
expect(alwaysIcon?.getAttribute("class")).toContain("text-muted-foreground");
|
||||||
|
|
||||||
|
expect(denySpan?.textContent).toBe("Deny");
|
||||||
|
expect(denySpan?.className).toContain("text-foreground");
|
||||||
|
expect(denySpan?.className).not.toContain("text-primary-foreground");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
allowButton?.click();
|
||||||
|
});
|
||||||
|
expect(onAnswer).toHaveBeenCalledWith("allow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses text-foreground for non-approval action buttons", async () => {
|
||||||
|
const onAnswer = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const block: AskBlock = {
|
||||||
|
kind: "ask",
|
||||||
|
text: "Which branch should we deploy?",
|
||||||
|
status: "pending",
|
||||||
|
actions: [
|
||||||
|
{ id: "main", label: "main" },
|
||||||
|
{ id: "staging", label: "staging" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<AskCard block={block} canAnswer={true} onAnswer={onAnswer} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionSpans = Array.from(container.querySelectorAll("button span"));
|
||||||
|
expect(actionSpans).toHaveLength(2);
|
||||||
|
for (const span of actionSpans) {
|
||||||
|
expect(span.className).toContain("text-foreground");
|
||||||
|
expect(span.className).not.toContain("text-primary-foreground");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -174,20 +174,27 @@ export function AskCard({
|
|||||||
</form>
|
</form>
|
||||||
) : askActions?.length ? (
|
) : askActions?.length ? (
|
||||||
<div className="mt-3.5 space-y-1.5">
|
<div className="mt-3.5 space-y-1.5">
|
||||||
{askActions.map((action) => (
|
{askActions.map((action) => {
|
||||||
|
const isPrimaryApproval = Boolean(approvalActions && action.id === "allow");
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={action.id}
|
key={action.id}
|
||||||
variant={approvalActions && action.id === "allow" ? "default" : "outline"}
|
variant={isPrimaryApproval ? "default" : "outline"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto w-full justify-between gap-3 whitespace-normal rounded-xl px-4 py-3 text-start font-medium transition-all group/ask-btn",
|
"h-auto w-full justify-between gap-3 whitespace-normal rounded-xl px-4 py-3 text-start font-medium transition-all group/ask-btn",
|
||||||
approvalActions && action.id === "allow"
|
isPrimaryApproval
|
||||||
? ""
|
? ""
|
||||||
: "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]",
|
: "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]",
|
||||||
)}
|
)}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onClick={() => void submitAnswer(action.id)}
|
onClick={() => void submitAnswer(action.id)}
|
||||||
>
|
>
|
||||||
<span className="flex-1 text-ui-body font-medium leading-snug text-foreground">
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex-1 text-ui-body font-medium leading-snug",
|
||||||
|
isPrimaryApproval ? "text-primary-foreground" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{pendingAction === action.id ? (
|
{pendingAction === action.id ? (
|
||||||
<Trans>Sending…</Trans>
|
<Trans>Sending…</Trans>
|
||||||
) : approvalActions ? (
|
) : approvalActions ? (
|
||||||
@ -198,10 +205,16 @@ export function AskCard({
|
|||||||
</span>
|
</span>
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
className="size-icon-sm shrink-0 text-muted-foreground transition-transform group-hover/ask-btn:translate-x-0.5 group-hover/ask-btn:text-foreground"
|
className={cn(
|
||||||
|
"size-icon-sm shrink-0 transition-transform group-hover/ask-btn:translate-x-0.5",
|
||||||
|
isPrimaryApproval
|
||||||
|
? "text-primary-foreground/75 group-hover/ask-btn:text-primary-foreground"
|
||||||
|
: "text-muted-foreground group-hover/ask-btn:text-foreground",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{!approvalActions ? (
|
{!approvalActions ? (
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<Button
|
<Button
|
||||||
@ -226,7 +239,11 @@ export function AskCard({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error ? <p className="mt-3 text-ui-body-sm text-destructive">{error}</p> : null}
|
{error ? (
|
||||||
|
<p role="alert" className="mt-3 text-ui-body-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { MessageBlock } from "@rakazo/contracts";
|
|||||||
import { cloudAgentHttpsUrl } from "@rakazo/core";
|
import { cloudAgentHttpsUrl } from "@rakazo/core";
|
||||||
import { Badge } from "@rakazo/ui-web/components/ui/badge";
|
import { Badge } from "@rakazo/ui-web/components/ui/badge";
|
||||||
import { Card, CardContent } from "@rakazo/ui-web/components/ui/card";
|
import { Card, CardContent } from "@rakazo/ui-web/components/ui/card";
|
||||||
|
import { ArrowUpRight } from "lucide-react";
|
||||||
|
|
||||||
export function CloudAgentCard({
|
export function CloudAgentCard({
|
||||||
block,
|
block,
|
||||||
@ -15,8 +16,14 @@ export function CloudAgentCard({
|
|||||||
<Card size="sm" className="w-80 max-w-full" data-testid="cloud-agent-card">
|
<Card size="sm" className="w-80 max-w-full" data-testid="cloud-agent-card">
|
||||||
<CardContent className="flex flex-col gap-2">
|
<CardContent className="flex flex-col gap-2">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<span className="font-medium" dir="auto">
|
<span className="inline-flex items-center gap-1 font-medium" dir="auto">
|
||||||
{block.title}
|
{block.title}
|
||||||
|
{href ? (
|
||||||
|
<ArrowUpRight
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-3.5 shrink-0 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@ -52,7 +59,13 @@ export function CloudAgentCard({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
return href ? (
|
return href ? (
|
||||||
<a href={href} target="_blank" rel="noreferrer" className="block max-w-full no-underline">
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
data-external-link=""
|
||||||
|
className="block max-w-full no-underline"
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export function DualSurfaceDivider({
|
|||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex w-3 md:w-4 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-border/0 hover:bg-accent/40 active:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
"group relative flex w-3 md:w-3 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||||
disabled && "pointer-events-none cursor-default opacity-50",
|
disabled && "pointer-events-none cursor-default opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@ -82,8 +82,8 @@ export function DualSurfaceDivider({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-1 rounded-full bg-border transition-colors group-hover:bg-accent group-active:bg-primary",
|
"h-12 w-1 rounded-full bg-border opacity-0 transition-opacity group-hover:opacity-100 group-active:bg-primary group-active:opacity-100",
|
||||||
isDragging && "bg-primary",
|
isDragging && "bg-primary opacity-100",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,13 @@ import { Input, Switch } from "@rakazo/ui-web";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useId, useRef, useState } from "react";
|
import { useEffect, useId, useRef, useState } from "react";
|
||||||
import { rpc } from "../lib/rpc";
|
import { rpc } from "../lib/rpc";
|
||||||
import { disableWebPush, enableWebPush, webPushEnabled, webPushSupported } from "../lib/web-push";
|
import {
|
||||||
|
disableWebPush,
|
||||||
|
enableWebPush,
|
||||||
|
isIosBrowserNeedsHomeScreen,
|
||||||
|
webPushEnabled,
|
||||||
|
webPushSupported,
|
||||||
|
} from "../lib/web-push";
|
||||||
import { SettingsSection } from "./SettingsSection";
|
import { SettingsSection } from "./SettingsSection";
|
||||||
|
|
||||||
const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" };
|
const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" };
|
||||||
@ -51,6 +57,7 @@ function SettingRow({
|
|||||||
export function NotificationSettingsPanel() {
|
export function NotificationSettingsPanel() {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const pushSupported = webPushSupported();
|
const pushSupported = webPushSupported();
|
||||||
|
const iosNeedsHomeScreen = isIosBrowserNeedsHomeScreen();
|
||||||
const [settings, setSettings] = useState<NotificationSettings | null>(null);
|
const [settings, setSettings] = useState<NotificationSettings | null>(null);
|
||||||
const [push, setPush] = useState<PushState>("loading");
|
const [push, setPush] = useState<PushState>("loading");
|
||||||
const [pushBusy, setPushBusy] = useState(false);
|
const [pushBusy, setPushBusy] = useState(false);
|
||||||
@ -153,6 +160,15 @@ export function NotificationSettingsPanel() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
) : iosNeedsHomeScreen ? (
|
||||||
|
<li data-testid="notification-ios-pwa-hint" className="py-3 first:pt-0 last:pb-0">
|
||||||
|
<div className="text-ui-body text-foreground">
|
||||||
|
<Trans>Push on this device</Trans>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-ui-body-sm text-muted-foreground">
|
||||||
|
<Trans>Add evOo to your Home Screen via Share to enable push on iOS.</Trans>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
) : null}
|
) : null}
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label={<Trans>Quiet hours</Trans>}
|
label={<Trans>Quiet hours</Trans>}
|
||||||
|
|||||||
@ -254,10 +254,10 @@ export function OoAtelierDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="max-h-[92vh] w-[420px] max-w-full gap-3 overflow-y-auto rounded-3xl p-5 sm:max-w-[420px]"
|
className="flex max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[420px] flex-col gap-3 overflow-hidden rounded-3xl p-5 sm:max-w-[420px]"
|
||||||
data-testid="oo-atelier"
|
data-testid="oo-atelier"
|
||||||
>
|
>
|
||||||
<DialogHeader className="flex-row items-center justify-between space-y-0">
|
<DialogHeader className="shrink-0 flex-row items-center justify-between space-y-0">
|
||||||
<DialogTitle className="text-ui-body font-semibold tracking-tight">
|
<DialogTitle className="text-ui-body font-semibold tracking-tight">
|
||||||
<Trans>Atelier</Trans>
|
<Trans>Atelier</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
@ -274,19 +274,19 @@ export function OoAtelierDialog({
|
|||||||
</button>
|
</button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex justify-center py-1" data-testid="oo-atelier-preview">
|
<div className="flex shrink-0 justify-center py-1" data-testid="oo-atelier-preview">
|
||||||
<BotAvatar
|
<BotAvatar
|
||||||
color={isPicture && !draft ? value : encoded}
|
color={isPicture && !draft ? value : encoded}
|
||||||
identity={identity}
|
identity={identity}
|
||||||
status={status}
|
status={status}
|
||||||
size={128}
|
size={120}
|
||||||
moment={preview}
|
moment={preview}
|
||||||
interactive
|
interactive
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="body">
|
<Tabs defaultValue="body" className="flex min-h-0 flex-1 flex-col">
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full shrink-0">
|
||||||
<TabsTrigger value="body">
|
<TabsTrigger value="body">
|
||||||
<Trans>Body</Trans>
|
<Trans>Body</Trans>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -303,7 +303,11 @@ export function OoAtelierDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="body" className="space-y-4 pt-2" data-testid="oo-atelier-body">
|
<TabsContent
|
||||||
|
value="body"
|
||||||
|
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||||
|
data-testid="oo-atelier-body"
|
||||||
|
>
|
||||||
<OptionGroup
|
<OptionGroup
|
||||||
title={t`Species`}
|
title={t`Species`}
|
||||||
values={OO_SPECIES}
|
values={OO_SPECIES}
|
||||||
@ -397,7 +401,11 @@ export function OoAtelierDialog({
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="eyes" className="space-y-4 pt-2" data-testid="oo-atelier-eyes">
|
<TabsContent
|
||||||
|
value="eyes"
|
||||||
|
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||||
|
data-testid="oo-atelier-eyes"
|
||||||
|
>
|
||||||
<OptionGroup
|
<OptionGroup
|
||||||
title={t`Lashes`}
|
title={t`Lashes`}
|
||||||
values={OO_LASHES}
|
values={OO_LASHES}
|
||||||
@ -456,7 +464,11 @@ export function OoAtelierDialog({
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="extras" className="space-y-4 pt-2" data-testid="oo-atelier-extras">
|
<TabsContent
|
||||||
|
value="extras"
|
||||||
|
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||||
|
data-testid="oo-atelier-extras"
|
||||||
|
>
|
||||||
<OptionGroup
|
<OptionGroup
|
||||||
title={t`Glasses`}
|
title={t`Glasses`}
|
||||||
values={OO_EYEWEAR}
|
values={OO_EYEWEAR}
|
||||||
@ -492,7 +504,7 @@ export function OoAtelierDialog({
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{allowUpload ? (
|
{allowUpload ? (
|
||||||
<TabsContent value="upload" className="pt-2">
|
<TabsContent value="upload" className="min-h-0 flex-1 overflow-y-auto pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Image upload area`}
|
aria-label={t`Image upload area`}
|
||||||
@ -534,7 +546,7 @@ export function OoAtelierDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<DialogFooter className="flex-row items-center justify-between border-border sm:justify-between">
|
<DialogFooter className="-mx-5 -mb-5 shrink-0 flex-row items-center justify-between border-border px-5 py-3.5 sm:justify-between">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { Trans, useLingui } from "@lingui/react/macro";
|
|||||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
||||||
import { Button, cn } from "@rakazo/ui-web";
|
import { Button, cn } from "@rakazo/ui-web";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { agentPointerFeed } from "../../lib/agent-pointer";
|
||||||
import { LocalBrowserPreview } from "./LocalBrowserPreview";
|
import { LocalBrowserPreview } from "./LocalBrowserPreview";
|
||||||
import { OoComputerBrowser } from "./OoComputerBrowser";
|
import { OoComputerBrowser } from "./OoComputerBrowser";
|
||||||
|
|
||||||
@ -16,8 +17,9 @@ export function InAppBrowser({
|
|||||||
computerScreenError,
|
computerScreenError,
|
||||||
sandboxProvider,
|
sandboxProvider,
|
||||||
booting,
|
booting,
|
||||||
onBoot,
|
|
||||||
onRefreshScreen,
|
onRefreshScreen,
|
||||||
|
onConnectScreen,
|
||||||
|
onOpenComputer,
|
||||||
hideCloseButton = false,
|
hideCloseButton = false,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -27,13 +29,21 @@ export function InAppBrowser({
|
|||||||
computerScreenError: React.ReactNode;
|
computerScreenError: React.ReactNode;
|
||||||
sandboxProvider: string | undefined;
|
sandboxProvider: string | undefined;
|
||||||
booting: boolean;
|
booting: boolean;
|
||||||
onBoot: () => Promise<void>;
|
|
||||||
onRefreshScreen: () => Promise<unknown>;
|
onRefreshScreen: () => Promise<unknown>;
|
||||||
|
onConnectScreen: () => Promise<unknown>;
|
||||||
|
onOpenComputer?: () => void;
|
||||||
hideCloseButton?: boolean;
|
hideCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [mode, setMode] = useState<InAppBrowserMode>("oo");
|
const [mode, setMode] = useState<InAppBrowserMode>("oo");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active?.id) return;
|
||||||
|
return agentPointerFeed.subscribe(active.id, () => {
|
||||||
|
setMode("oo");
|
||||||
|
});
|
||||||
|
}, [active?.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col" data-testid="in-app-browser">
|
<div className="flex h-full min-h-0 flex-col" data-testid="in-app-browser">
|
||||||
<div className="flex items-center justify-between gap-2 border-b px-2 py-1">
|
<div className="flex items-center justify-between gap-2 border-b px-2 py-1">
|
||||||
@ -83,8 +93,9 @@ export function InAppBrowser({
|
|||||||
computerScreenError={computerScreenError}
|
computerScreenError={computerScreenError}
|
||||||
sandboxProvider={sandboxProvider}
|
sandboxProvider={sandboxProvider}
|
||||||
booting={booting}
|
booting={booting}
|
||||||
onBoot={onBoot}
|
|
||||||
onRefreshScreen={onRefreshScreen}
|
onRefreshScreen={onRefreshScreen}
|
||||||
|
onConnectScreen={onConnectScreen}
|
||||||
|
onOpenComputer={onOpenComputer}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid flex-1 place-items-center px-6 text-center text-ui-body-sm text-muted-foreground">
|
<div className="grid flex-1 place-items-center px-6 text-center text-ui-body-sm text-muted-foreground">
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
||||||
import { Button, cn, Input } from "@rakazo/ui-web";
|
import { Button, cn, Input } from "@rakazo/ui-web";
|
||||||
import { ArrowLeft, ArrowRight, ExternalLink, Loader2, RotateCw } from "lucide-react";
|
import { ArrowLeft, ArrowRight, ExternalLink, Loader2, Maximize2, RotateCw } from "lucide-react";
|
||||||
import { type FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
import { type FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { agentPointerFeed } from "../../lib/agent-pointer";
|
||||||
import { normalizeBrowserUrl } from "../../lib/browser-url";
|
import { normalizeBrowserUrl } from "../../lib/browser-url";
|
||||||
import { rpc } from "../../lib/rpc";
|
import { rpc } from "../../lib/rpc";
|
||||||
import { screenIframeSandbox } from "../../pages/shell/computer-screen";
|
import { screenIframeSandbox } from "../../pages/shell/computer-screen";
|
||||||
@ -33,8 +34,9 @@ export function OoComputerBrowser({
|
|||||||
computerScreenError,
|
computerScreenError,
|
||||||
sandboxProvider,
|
sandboxProvider,
|
||||||
booting,
|
booting,
|
||||||
onBoot,
|
|
||||||
onRefreshScreen,
|
onRefreshScreen,
|
||||||
|
onConnectScreen,
|
||||||
|
onOpenComputer,
|
||||||
}: {
|
}: {
|
||||||
active: Bot;
|
active: Bot;
|
||||||
computer: ComputerStatus | null;
|
computer: ComputerStatus | null;
|
||||||
@ -42,8 +44,11 @@ export function OoComputerBrowser({
|
|||||||
computerScreenError: React.ReactNode;
|
computerScreenError: React.ReactNode;
|
||||||
sandboxProvider: string | undefined;
|
sandboxProvider: string | undefined;
|
||||||
booting: boolean;
|
booting: boolean;
|
||||||
onBoot: () => Promise<void>;
|
|
||||||
onRefreshScreen: () => Promise<unknown>;
|
onRefreshScreen: () => Promise<unknown>;
|
||||||
|
/** Fetches the screen of an already running computer, without touching the thread. */
|
||||||
|
onConnectScreen: () => Promise<unknown>;
|
||||||
|
/** Opens the full computer window, where the user can take control or teach. */
|
||||||
|
onOpenComputer?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [chrome, setChrome] = useState<ChromeState>(EMPTY_CHROME);
|
const [chrome, setChrome] = useState<ChromeState>(EMPTY_CHROME);
|
||||||
@ -73,10 +78,26 @@ export function OoComputerBrowser({
|
|||||||
}, [computer?.state, refreshChrome]);
|
}, [computer?.state, refreshChrome]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (computer?.state === "running" && (embeddedScreenUrl || computer?.kind === "desktop"))
|
if (computer?.state !== "running") return;
|
||||||
return;
|
let settleTimer: number | undefined;
|
||||||
void onBoot().catch(() => undefined);
|
const unsubscribe = agentPointerFeed.subscribe(active.id, () => {
|
||||||
}, [active.id, computer?.kind, computer?.state, embeddedScreenUrl, onBoot]);
|
void refreshChrome();
|
||||||
|
window.clearTimeout(settleTimer);
|
||||||
|
settleTimer = window.setTimeout(() => void refreshChrome(), 600);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
window.clearTimeout(settleTimer);
|
||||||
|
};
|
||||||
|
}, [active.id, computer?.state, refreshChrome]);
|
||||||
|
|
||||||
|
const connectScreen = useRef(onConnectScreen);
|
||||||
|
connectScreen.current = onConnectScreen;
|
||||||
|
// Showing the browser never starts a stopped computer; it only reconnects a running screen.
|
||||||
|
useEffect(() => {
|
||||||
|
if (computer?.state !== "running" || embeddedScreenUrl || computer?.kind === "desktop") return;
|
||||||
|
void connectScreen.current().catch(() => undefined);
|
||||||
|
}, [active.id, computer?.kind, computer?.state, embeddedScreenUrl]);
|
||||||
|
|
||||||
const runNav = useCallback(
|
const runNav = useCallback(
|
||||||
async (task: () => Promise<ChromeState>) => {
|
async (task: () => Promise<ChromeState>) => {
|
||||||
@ -200,6 +221,18 @@ export function OoComputerBrowser({
|
|||||||
>
|
>
|
||||||
<ExternalLink strokeWidth={1.7} className="size-icon-md" />
|
<ExternalLink strokeWidth={1.7} className="size-icon-md" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{onOpenComputer && !unavailable && !computerScreenError ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
aria-label={t`Open in full window`}
|
||||||
|
data-testid="computer-open"
|
||||||
|
onClick={onOpenComputer}
|
||||||
|
>
|
||||||
|
<Maximize2 strokeWidth={1.7} className="size-icon-md" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -15,14 +15,14 @@ type OnboardingDiscoveryShellProps = {
|
|||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]): string {
|
function sourceLabel(id: DiscoverySourceId): ReactNode {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "profile-basics":
|
case "profile-basics":
|
||||||
return t`Your profile`;
|
return <Trans>Your profile</Trans>;
|
||||||
case "connected-tools":
|
case "connected-tools":
|
||||||
return t`Connected tools`;
|
return <Trans>Connected tools</Trans>;
|
||||||
case "specialist-crew":
|
case "specialist-crew":
|
||||||
return t`Specialist crew`;
|
return <Trans>Specialist crew</Trans>;
|
||||||
default: {
|
default: {
|
||||||
const never: never = id;
|
const never: never = id;
|
||||||
return never;
|
return never;
|
||||||
@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({
|
|||||||
>
|
>
|
||||||
{active ? (
|
{active ? (
|
||||||
<>
|
<>
|
||||||
{sourceLabel(active.id, t)}
|
{sourceLabel(active.id)}
|
||||||
{active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? (
|
{active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? (
|
||||||
<>
|
<>
|
||||||
{" · "}
|
{" · "}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export function OnboardingOoDesigner({ config, onChange, onContinue }: Onboardin
|
|||||||
<Trans>What feels like you?</Trans>
|
<Trans>What feels like you?</Trans>
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
className="mt-3 flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
className="mt-3 flex flex-wrap gap-2 pb-1"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label={t`Look families`}
|
aria-label={t`Look families`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export function VoiceLiveText({ text, reducedMotion }: { text: string; reducedMo
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
dir="auto"
|
dir="auto"
|
||||||
className="text-balance text-center text-ui-title font-medium leading-snug text-foreground"
|
className="line-clamp-3 text-pretty text-start text-ui-body font-medium leading-snug text-foreground"
|
||||||
>
|
>
|
||||||
{captionWords(text).map((word, index) => (
|
{captionWords(text).map((word, index) => (
|
||||||
<span key={`${utterance}-${index}`} className={reducedMotion ? undefined : "voice-word-in"}>
|
<span key={`${utterance}-${index}`} className={reducedMotion ? undefined : "voice-word-in"}>
|
||||||
|
|||||||
@ -90,21 +90,52 @@ export function VoiceStage({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
data-testid="call-view"
|
data-testid="call-view"
|
||||||
data-theme="dark"
|
data-theme="dark"
|
||||||
data-phase={phase}
|
data-phase={phase}
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
className="fixed inset-0 z-40 flex flex-col items-center justify-center gap-3 overflow-y-auto bg-background px-4 pt-[max(1rem,env(safe-area-inset-top))] pb-[max(1rem,env(safe-area-inset-bottom))] text-foreground"
|
className="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex flex-col items-center justify-end gap-2.5 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] text-foreground"
|
||||||
>
|
>
|
||||||
<h1 id={titleId} className="sr-only">
|
<h1 id={titleId} className="sr-only">
|
||||||
{botName}
|
{botName}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="voice-transcript-panel"
|
||||||
|
className={`pointer-events-auto w-full max-w-lg overflow-hidden transition-[max-height,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||||
|
transcriptOpen ? "max-h-56 opacity-100" : "max-h-0 opacity-0 pointer-events-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="rk-scroll max-h-52 overflow-y-auto rounded-3xl border border-border bg-card/95 px-5 py-4 text-start shadow-xl backdrop-blur-xl">
|
||||||
|
{transcriptLines.length ? (
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
{transcriptLines.map((line, index) => (
|
||||||
|
<li
|
||||||
|
key={`${line.role}-${index}`}
|
||||||
|
className={`text-ui-body-sm leading-relaxed ${
|
||||||
|
line.role === "user" ? "text-foreground" : "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-ui-label text-muted-foreground">
|
||||||
|
{line.role === "user" ? t`You` : (line.speakerName ?? botName)}
|
||||||
|
</span>
|
||||||
|
<span dir="auto" className="mt-0.5 block">
|
||||||
|
{line.text}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-ui-body-sm text-muted-foreground">
|
||||||
|
<Trans>Your conversation will appear here.</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{awarenessLabel && awarenessEvent ? (
|
{awarenessLabel && awarenessEvent ? (
|
||||||
<div className="w-full max-w-md">
|
<div className="pointer-events-auto w-full max-w-lg">
|
||||||
<VoiceAwarenessStrip
|
<VoiceAwarenessStrip
|
||||||
event={awarenessEvent}
|
event={awarenessEvent}
|
||||||
label={awarenessLabel}
|
label={awarenessLabel}
|
||||||
@ -116,7 +147,7 @@ export function VoiceStage({
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
data-testid="voice-panel"
|
data-testid="voice-panel"
|
||||||
className={`relative isolate flex min-h-[min(68vh,30rem)] w-full max-w-md flex-col overflow-hidden rounded-[2rem] border border-border bg-card shadow-2xl ${reducedMotion ? "" : "voice-panel-in"}`}
|
className={`pointer-events-auto relative isolate flex w-full max-w-lg flex-col overflow-hidden rounded-3xl border border-border bg-card/95 shadow-2xl backdrop-blur-xl ${reducedMotion ? "" : "voice-panel-in"}`}
|
||||||
>
|
>
|
||||||
<VoiceAurora
|
<VoiceAurora
|
||||||
botColor={botColor}
|
botColor={botColor}
|
||||||
@ -125,28 +156,30 @@ export function VoiceStage({
|
|||||||
reducedMotion={reducedMotion}
|
reducedMotion={reducedMotion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex flex-1 flex-col items-center justify-center gap-6 px-7 pt-10 pb-4">
|
<div className="relative flex items-center gap-3.5 px-4 pt-3.5 pb-2.5">
|
||||||
<div title={`${botName} · ${botTitle}`}>
|
<div className="shrink-0" title={`${botName} · ${botTitle}`}>
|
||||||
<BotAvatar
|
<BotAvatar
|
||||||
color={botColor}
|
color={botColor}
|
||||||
identity={botId}
|
identity={botId}
|
||||||
status={botStatus}
|
status={botStatus}
|
||||||
pose={mascotPose}
|
pose={mascotPose}
|
||||||
size={88}
|
size={44}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 text-start">
|
||||||
<VoiceLiveText text={subtitle} reducedMotion={reducedMotion} />
|
<VoiceLiveText text={subtitle} reducedMotion={reducedMotion} />
|
||||||
{error ? (
|
{error ? (
|
||||||
<p role="alert" className="text-center text-ui-body-sm font-medium text-destructive">
|
<p role="alert" className="mt-1 text-ui-body-sm font-medium text-destructive">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative flex items-center gap-2 px-4 pb-4">
|
<div className="relative flex items-center gap-2 px-4 pb-3">
|
||||||
<span
|
<span
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
className="mr-auto truncate rounded-full bg-foreground/10 px-3.5 py-2 text-ui-body-sm font-medium text-foreground/85 backdrop-blur-md"
|
className="mr-auto truncate rounded-full bg-foreground/10 px-3 py-1.5 text-ui-body-sm font-medium text-foreground/85 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
{phaseLabel}
|
{phaseLabel}
|
||||||
</span>
|
</span>
|
||||||
@ -187,39 +220,6 @@ export function VoiceStage({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
<div
|
|
||||||
id="voice-transcript-panel"
|
|
||||||
className={`w-full max-w-md overflow-hidden transition-[max-height,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
|
||||||
transcriptOpen ? "max-h-56 opacity-100" : "max-h-0 opacity-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="rk-scroll max-h-52 overflow-y-auto rounded-3xl border border-border bg-card px-5 py-4 text-start">
|
|
||||||
{transcriptLines.length ? (
|
|
||||||
<ul className="space-y-2.5">
|
|
||||||
{transcriptLines.map((line, index) => (
|
|
||||||
<li
|
|
||||||
key={`${line.role}-${index}`}
|
|
||||||
className={`text-ui-body-sm leading-relaxed ${
|
|
||||||
line.role === "user" ? "text-foreground" : "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-ui-label text-muted-foreground">
|
|
||||||
{line.role === "user" ? t`You` : (line.speakerName ?? botName)}
|
|
||||||
</span>
|
|
||||||
<span dir="auto" className="mt-0.5 block">
|
|
||||||
{line.text}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="text-ui-body-sm text-muted-foreground">
|
|
||||||
<Trans>Your conversation will appear here.</Trans>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,4 +42,20 @@ describe("ChatMarkdown", () => {
|
|||||||
expect(html).toContain('aria-label="Copy code"');
|
expect(html).toContain('aria-label="Copy code"');
|
||||||
expect(html).toContain("rk-chat-markdown-copy");
|
expect(html).toContain("rk-chat-markdown-copy");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks outbound links with target=_blank, external indicator, and renders inline hex color swatches", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<ChatMarkdown>
|
||||||
|
{"See [Preview](https://example.com/review) and palette `#6B4226` or `#E8722A`."}
|
||||||
|
</ChatMarkdown>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain('href="https://example.com/review"');
|
||||||
|
expect(html).toContain('target="_blank"');
|
||||||
|
expect(html).toContain('rel="noreferrer noopener"');
|
||||||
|
expect(html).toContain("rk-chat-markdown-external-link");
|
||||||
|
expect(html).toContain("rk-chat-markdown-external-icon");
|
||||||
|
expect(html).toContain('data-testid="markdown-color-swatch"');
|
||||||
|
expect(html).toContain('data-color="#6B4226"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,6 +9,37 @@ import {
|
|||||||
StreamingVad,
|
StreamingVad,
|
||||||
} from "@rakazo/core";
|
} from "@rakazo/core";
|
||||||
import { selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
import { selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
||||||
|
import { readUiLocale } from "./ui-locale.js";
|
||||||
|
|
||||||
|
const SPEECH_LOCALE_TAGS: Record<string, string> = {
|
||||||
|
de: "de-DE",
|
||||||
|
en: "en-US",
|
||||||
|
es: "es-ES",
|
||||||
|
fr: "fr-FR",
|
||||||
|
hi: "hi-IN",
|
||||||
|
it: "it-IT",
|
||||||
|
ja: "ja-JP",
|
||||||
|
ko: "ko-KR",
|
||||||
|
"pt-BR": "pt-BR",
|
||||||
|
ru: "ru-RU",
|
||||||
|
tr: "tr-TR",
|
||||||
|
"zh-CN": "zh-CN",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function speechRecognitionLang(): string {
|
||||||
|
const uiLocale = readUiLocale();
|
||||||
|
const mapped = uiLocale ? SPEECH_LOCALE_TAGS[uiLocale] : undefined;
|
||||||
|
if (uiLocale && mapped) {
|
||||||
|
// Prefer browser's regional variant when it matches the UI language (e.g. de-AT / de-CH)
|
||||||
|
const navLang = typeof navigator !== "undefined" ? navigator.language : "";
|
||||||
|
const primary = uiLocale.split("-")[0]?.toLowerCase() ?? "";
|
||||||
|
if (navLang?.toLowerCase().startsWith(`${primary}-`)) {
|
||||||
|
return navLang;
|
||||||
|
}
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
return (typeof navigator !== "undefined" && navigator.language) || "de-DE";
|
||||||
|
}
|
||||||
|
|
||||||
export type DictationMode = "hold" | "endpoint";
|
export type DictationMode = "hold" | "endpoint";
|
||||||
|
|
||||||
@ -205,7 +236,7 @@ export class Dictation {
|
|||||||
const rec = new Ctor();
|
const rec = new Ctor();
|
||||||
rec.continuous = true;
|
rec.continuous = true;
|
||||||
rec.interimResults = true;
|
rec.interimResults = true;
|
||||||
rec.lang = navigator.language || "en-US";
|
rec.lang = speechRecognitionLang();
|
||||||
rec.onresult = (event) => {
|
rec.onresult = (event) => {
|
||||||
if (this.token !== mine) return;
|
if (this.token !== mine) return;
|
||||||
let transcript = "";
|
let transcript = "";
|
||||||
|
|||||||
@ -98,13 +98,25 @@ describe("isToday", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("buildHeuteBrief", () => {
|
describe("buildHeuteBrief", () => {
|
||||||
it("collects needs-you, doing, and done buckets", () => {
|
it("collects needs-you, doing, and done buckets, ignoring passive unread chats that do not need the user", () => {
|
||||||
const brief = buildHeuteBrief({
|
const brief = buildHeuteBrief({
|
||||||
bots: [
|
bots: [
|
||||||
bot({ id: "bot-1", unread: true }),
|
bot({ id: "bot-1", unread: true, status: "waiting_input" }),
|
||||||
bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }),
|
bot({
|
||||||
|
id: "bot-2",
|
||||||
|
unread: true,
|
||||||
|
status: "waiting_takeover",
|
||||||
|
updatedAt: "2026-09-22T08:00:00.000Z",
|
||||||
|
name: "Steve",
|
||||||
|
}),
|
||||||
|
bot({
|
||||||
|
id: "bot-passive",
|
||||||
|
unread: true,
|
||||||
|
status: "idle",
|
||||||
|
name: "PassiveBot",
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z" })],
|
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z", unread: true })],
|
||||||
activeRuns: [
|
activeRuns: [
|
||||||
run({ runId: "run-1", botId: "bot-1" }),
|
run({ runId: "run-1", botId: "bot-1" }),
|
||||||
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
|
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
|
||||||
@ -157,7 +169,7 @@ describe("buildHeuteBrief", () => {
|
|||||||
|
|
||||||
it("caps needs-you at three items", () => {
|
it("caps needs-you at three items", () => {
|
||||||
const bots = Array.from({ length: 5 }, (_, index) =>
|
const bots = Array.from({ length: 5 }, (_, index) =>
|
||||||
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
|
bot({ id: `bot-${index}`, unread: true, status: "waiting_input", name: `Bot ${index}` }),
|
||||||
);
|
);
|
||||||
const brief = buildHeuteBrief({
|
const brief = buildHeuteBrief({
|
||||||
bots,
|
bots,
|
||||||
@ -169,6 +181,57 @@ describe("buildHeuteBrief", () => {
|
|||||||
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
|
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
|
||||||
expect(brief.actionCount).toBe(5);
|
expect(brief.actionCount).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges and deduplicates actionable unread bots and groups across spaces", () => {
|
||||||
|
const brief = buildHeuteBrief({
|
||||||
|
bots: [bot({ id: "bot-1", spaceId: "space-2", unread: false, status: "idle" })],
|
||||||
|
groups: [],
|
||||||
|
spaces: [
|
||||||
|
{
|
||||||
|
id: "space-1",
|
||||||
|
bots: [
|
||||||
|
bot({
|
||||||
|
id: "bot-home-1",
|
||||||
|
spaceId: "space-1",
|
||||||
|
unread: true,
|
||||||
|
status: "waiting_input",
|
||||||
|
name: "Code Architect",
|
||||||
|
}),
|
||||||
|
bot({
|
||||||
|
id: "bot-home-2",
|
||||||
|
spaceId: "space-1",
|
||||||
|
unread: true,
|
||||||
|
status: "waiting_takeover",
|
||||||
|
name: "UI Designer",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
group({
|
||||||
|
id: "group-home-1",
|
||||||
|
spaceId: "space-1",
|
||||||
|
unread: true,
|
||||||
|
leadBotId: "bot-home-1",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "space-2",
|
||||||
|
// Current space's optimistic `bots` entry (unread: false) takes precedence over stale space snapshot
|
||||||
|
bots: [bot({ id: "bot-1", spaceId: "space-2", unread: true, status: "waiting_input" })],
|
||||||
|
groups: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeRuns: [],
|
||||||
|
recentRuns: [],
|
||||||
|
now,
|
||||||
|
});
|
||||||
|
expect(brief.actionCount).toBe(3);
|
||||||
|
expect(brief.needsYou.map((item) => (item.kind === "unread" ? item.chat.id : ""))).toEqual([
|
||||||
|
"bot-home-1",
|
||||||
|
"bot-home-2",
|
||||||
|
"group-home-1",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function habit(overrides: Partial<HabitToday> = {}): HabitToday {
|
function habit(overrides: Partial<HabitToday> = {}): HabitToday {
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
import type { Bot, CoachToday, Group, RunActivityRow } from "@rakazo/contracts";
|
import type {
|
||||||
import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity";
|
Bot,
|
||||||
|
CoachToday,
|
||||||
|
Group,
|
||||||
|
RunActivityRow,
|
||||||
|
Space,
|
||||||
|
SpaceBot,
|
||||||
|
SpaceGroup,
|
||||||
|
} from "@rakazo/contracts";
|
||||||
|
import {
|
||||||
|
isActionableRun,
|
||||||
|
isDoingStatus,
|
||||||
|
isFailedRoutine,
|
||||||
|
isNeedsYouStatus,
|
||||||
|
} from "./agent-activity";
|
||||||
|
|
||||||
export type JetztRunItem = {
|
export type JetztRunItem = {
|
||||||
kind: "run";
|
kind: "run";
|
||||||
@ -9,7 +22,7 @@ export type JetztRunItem = {
|
|||||||
export type JetztUnreadItem = {
|
export type JetztUnreadItem = {
|
||||||
kind: "unread";
|
kind: "unread";
|
||||||
chatKind: "bot" | "group";
|
chatKind: "bot" | "group";
|
||||||
chat: Bot | Group;
|
chat: SpaceBot | (SpaceGroup & { leadBotId?: string | null });
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JetztItem = JetztRunItem | JetztUnreadItem;
|
export type JetztItem = JetztRunItem | JetztUnreadItem;
|
||||||
@ -76,16 +89,33 @@ function chatTargetKey(chatKind: "bot" | "group", chatId: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildHeuteBrief(params: {
|
export function buildHeuteBrief(params: {
|
||||||
bots: Bot[];
|
bots: readonly SpaceBot[];
|
||||||
groups: Group[];
|
groups: readonly (SpaceGroup & { leadBotId?: string | null })[];
|
||||||
|
spaces?: readonly Pick<Space, "id" | "bots" | "groups">[];
|
||||||
activeRuns: RunActivityRow[];
|
activeRuns: RunActivityRow[];
|
||||||
recentRuns: RunActivityRow[];
|
recentRuns: RunActivityRow[];
|
||||||
now?: Date;
|
now?: Date;
|
||||||
}): HeuteBriefData {
|
}): HeuteBriefData {
|
||||||
const { bots, groups, activeRuns, recentRuns, now = new Date() } = params;
|
const { bots, groups, spaces = [], activeRuns, recentRuns, now = new Date() } = params;
|
||||||
const needsYouAll: JetztItem[] = [];
|
const needsYouAll: JetztItem[] = [];
|
||||||
const coveredTargets = new Set<string>();
|
const coveredTargets = new Set<string>();
|
||||||
|
|
||||||
|
const seenBotIds = new Set<string>();
|
||||||
|
const allBots: SpaceBot[] = [];
|
||||||
|
for (const bot of [...bots, ...spaces.flatMap((space) => space.bots)]) {
|
||||||
|
if (seenBotIds.has(bot.id)) continue;
|
||||||
|
seenBotIds.add(bot.id);
|
||||||
|
allBots.push(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenGroupIds = new Set<string>();
|
||||||
|
const allGroups: (SpaceGroup & { leadBotId?: string | null })[] = [];
|
||||||
|
for (const group of [...groups, ...spaces.flatMap((space) => space.groups)]) {
|
||||||
|
if (seenGroupIds.has(group.id)) continue;
|
||||||
|
seenGroupIds.add(group.id);
|
||||||
|
allGroups.push(group);
|
||||||
|
}
|
||||||
|
|
||||||
const actionableRuns = [
|
const actionableRuns = [
|
||||||
...activeRuns.filter(isActionableRun),
|
...activeRuns.filter(isActionableRun),
|
||||||
...recentRuns.filter(isFailedRoutine),
|
...recentRuns.filter(isFailedRoutine),
|
||||||
@ -98,12 +128,20 @@ export function buildHeuteBrief(params: {
|
|||||||
coveredTargets.add(runTargetKey(run));
|
coveredTargets.add(runTargetKey(run));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const botStatusById = new Map(allBots.map((bot) => [bot.id, bot.status]));
|
||||||
const unreadChats: JetztUnreadItem[] = [
|
const unreadChats: JetztUnreadItem[] = [
|
||||||
...bots
|
...allBots
|
||||||
.filter((bot) => bot.unread)
|
.filter((bot) => bot.unread && isNeedsYouStatus(bot.status))
|
||||||
.map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
|
.map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
|
||||||
...groups
|
...allGroups
|
||||||
.filter((group) => group.unread)
|
.filter((group) => {
|
||||||
|
if (!group.unread) return false;
|
||||||
|
const memberBotIds = [
|
||||||
|
...(group.leadBotId ? [group.leadBotId] : []),
|
||||||
|
...(group.members?.map((m) => m.botId) ?? []),
|
||||||
|
];
|
||||||
|
return memberBotIds.some((id) => isNeedsYouStatus(botStatusById.get(id)));
|
||||||
|
})
|
||||||
.map((group) => ({
|
.map((group) => ({
|
||||||
kind: "unread" as const,
|
kind: "unread" as const,
|
||||||
chatKind: "group" as const,
|
chatKind: "group" as const,
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
type AudioContextCtor = new () => AudioContext;
|
type AudioContextCtor = new () => AudioContext;
|
||||||
|
|
||||||
/** Master output ~−15 LUFS for short procedural peaks. */
|
/** Master output ~−12 LUFS for clear, warm procedural peaks. */
|
||||||
const SFX_MASTER_GAIN = 0.58;
|
const SFX_MASTER_GAIN = 0.85;
|
||||||
/** Duck depth when voice lane active (TTS or mic wash). */
|
/** Duck depth when voice lane active (TTS or mic wash). */
|
||||||
const SFX_DUCK_RATIO = 0.28;
|
const SFX_DUCK_RATIO = 0.28;
|
||||||
const DUCK_RAMP_S = 0.065;
|
const DUCK_RAMP_S = 0.065;
|
||||||
@ -22,8 +22,10 @@ class OoAudioBus {
|
|||||||
private ctx: AudioContext | null = null;
|
private ctx: AudioContext | null = null;
|
||||||
private sfxGain: GainNode | null = null;
|
private sfxGain: GainNode | null = null;
|
||||||
private voiceHold = 0;
|
private voiceHold = 0;
|
||||||
|
private unlockBound = false;
|
||||||
|
|
||||||
ensureContext(): AudioContext | null {
|
ensureContext(): AudioContext | null {
|
||||||
|
this.bindGestureUnlock();
|
||||||
if (this.ctx && this.ctx.state !== "closed") return this.ctx;
|
if (this.ctx && this.ctx.state !== "closed") return this.ctx;
|
||||||
const Ctor = audioContextCtor();
|
const Ctor = audioContextCtor();
|
||||||
if (!Ctor) return null;
|
if (!Ctor) return null;
|
||||||
@ -43,6 +45,34 @@ class OoAudioBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Unlocks Web Audio on the user's first click, key, or tap so async SSE events can play audio. */
|
||||||
|
bindGestureUnlock(): void {
|
||||||
|
if (
|
||||||
|
this.unlockBound ||
|
||||||
|
typeof window === "undefined" ||
|
||||||
|
typeof window.addEventListener !== "function"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.unlockBound = true;
|
||||||
|
const unlock = () => {
|
||||||
|
const ctx = this.ensureContext();
|
||||||
|
if (ctx && ctx.state === "suspended") {
|
||||||
|
void ctx.resume().catch(() => undefined);
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
||||||
|
try {
|
||||||
|
window.speechSynthesis.getVoices?.();
|
||||||
|
} catch {
|
||||||
|
// Ignore voice preload errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("pointerdown", unlock, { capture: true, passive: true });
|
||||||
|
window.addEventListener("keydown", unlock, { capture: true, passive: true });
|
||||||
|
window.addEventListener("touchstart", unlock, { capture: true, passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
/** Route for oo-sound / oo-vocals / oo-voice-mark oscillators. */
|
/** Route for oo-sound / oo-vocals / oo-voice-mark oscillators. */
|
||||||
sfxDestination(ctx: AudioContext): AudioNode {
|
sfxDestination(ctx: AudioContext): AudioNode {
|
||||||
if (ctx !== this.ctx) {
|
if (ctx !== this.ctx) {
|
||||||
|
|||||||
@ -68,61 +68,62 @@ function playCueOnContext(ctx: AudioContext, cue: OoSoundCue) {
|
|||||||
|
|
||||||
switch (cue) {
|
switch (cue) {
|
||||||
case "app-start":
|
case "app-start":
|
||||||
sweep(ctx, master, 523.25, 659.25, t0, 0.14, { gain: 0.038, type: "sine" });
|
sweep(ctx, master, 523.25, 659.25, t0, 0.16, { gain: 0.11, type: "sine" });
|
||||||
sweep(ctx, master, 659.25, 493.88, t0 + 0.1, 0.18, { gain: 0.032, type: "triangle" });
|
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.2, { gain: 0.095, type: "triangle" });
|
||||||
break;
|
break;
|
||||||
case "interact":
|
case "interact":
|
||||||
tone(ctx, master, 1046.5, t0, 0.06, {
|
tone(ctx, master, 880, t0, 0.075, {
|
||||||
gain: 0.018,
|
gain: 0.065,
|
||||||
attack: 0.003,
|
attack: 0.004,
|
||||||
release: 0.04,
|
release: 0.045,
|
||||||
type: "triangle",
|
type: "triangle",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "success-resolve":
|
case "success-resolve":
|
||||||
case "work-complete":
|
case "work-complete":
|
||||||
// mint → gold resolve (~270 ms)
|
// mint → gold resolve (~270 ms)
|
||||||
sweep(ctx, master, 523.25, 659.25, t0, 0.12, { gain: 0.042, type: "sine" });
|
sweep(ctx, master, 523.25, 659.25, t0, 0.13, { gain: 0.12, type: "sine" });
|
||||||
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.036, type: "triangle" });
|
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.105, type: "triangle" });
|
||||||
tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.03, type: "sine" });
|
tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.09, type: "sine" });
|
||||||
break;
|
break;
|
||||||
case "need-you":
|
case "need-you":
|
||||||
sweep(ctx, master, 220, 330, t0, 0.1, { gain: 0.042, type: "triangle" });
|
sweep(ctx, master, 261.63, 392, t0, 0.12, { gain: 0.125, type: "triangle" });
|
||||||
tone(ctx, master, 277.18, t0 + 0.09, 0.12, { gain: 0.035, release: 0.08 });
|
tone(ctx, master, 523.25, t0 + 0.1, 0.18, { gain: 0.11, release: 0.1 });
|
||||||
break;
|
break;
|
||||||
case "listening-start":
|
case "listening-start":
|
||||||
case "voice_listen":
|
case "voice_listen":
|
||||||
tone(ctx, master, 220, t0, 0.22, { gain: 0.045, type: "triangle" });
|
tone(ctx, master, 293.66, t0, 0.2, { gain: 0.11, type: "triangle" });
|
||||||
tone(ctx, master, 330, t0 + 0.04, 0.28, { gain: 0.035 });
|
tone(ctx, master, 440, t0 + 0.05, 0.24, { gain: 0.095 });
|
||||||
break;
|
break;
|
||||||
case "listening-stop":
|
case "listening-stop":
|
||||||
tone(ctx, master, 330, t0, 0.18, { gain: 0.03, release: 0.12 });
|
tone(ctx, master, 392, t0, 0.16, { gain: 0.085, release: 0.1 });
|
||||||
|
tone(ctx, master, 523.25, t0 + 0.06, 0.18, { gain: 0.075, release: 0.11 });
|
||||||
break;
|
break;
|
||||||
case "speaking-start":
|
case "speaking-start":
|
||||||
tone(ctx, master, 440, t0, 0.2, { gain: 0.04, type: "sine" });
|
sweep(ctx, master, 392, 523.25, t0, 0.14, { gain: 0.095, type: "sine" });
|
||||||
break;
|
break;
|
||||||
case "speaking-done":
|
case "speaking-done":
|
||||||
tone(ctx, master, 523.25, t0, 0.24, { gain: 0.035 });
|
tone(ctx, master, 523.25, t0, 0.2, { gain: 0.085 });
|
||||||
tone(ctx, master, 392, t0 + 0.08, 0.3, { gain: 0.028, release: 0.14 });
|
tone(ctx, master, 392, t0 + 0.08, 0.26, { gain: 0.075, release: 0.14 });
|
||||||
break;
|
break;
|
||||||
case "voice_bargein":
|
case "voice_bargein":
|
||||||
sweep(ctx, master, 523.25, 261.63, t0, 0.05, { gain: 0.032, type: "sine" });
|
sweep(ctx, master, 523.25, 261.63, t0, 0.07, { gain: 0.09, type: "sine" });
|
||||||
break;
|
break;
|
||||||
case "attention":
|
case "attention":
|
||||||
tone(ctx, master, 277.18, t0, 0.16, { gain: 0.05, type: "triangle" });
|
tone(ctx, master, 329.63, t0, 0.16, { gain: 0.13, type: "triangle" });
|
||||||
tone(ctx, master, 233.08, t0 + 0.12, 0.22, { gain: 0.045 });
|
tone(ctx, master, 261.63, t0 + 0.12, 0.22, { gain: 0.115 });
|
||||||
break;
|
break;
|
||||||
case "message-sent":
|
case "message-sent":
|
||||||
tone(ctx, master, 880, t0, 0.1, { gain: 0.022, attack: 0.004, release: 0.06 });
|
sweep(ctx, master, 587.33, 880, t0, 0.11, { gain: 0.08, type: "sine" });
|
||||||
break;
|
break;
|
||||||
case "handoff":
|
case "handoff":
|
||||||
tone(ctx, master, 349.23, t0, 0.2, { gain: 0.04 });
|
tone(ctx, master, 392, t0, 0.18, { gain: 0.11 });
|
||||||
tone(ctx, master, 466.16, t0 + 0.1, 0.24, { gain: 0.035, type: "triangle" });
|
tone(ctx, master, 523.25, t0 + 0.09, 0.22, { gain: 0.1, type: "triangle" });
|
||||||
break;
|
break;
|
||||||
case "group-spark":
|
case "group-spark":
|
||||||
tone(ctx, master, 587.33, t0, 0.11, { gain: 0.032, attack: 0.004, type: "triangle" });
|
tone(ctx, master, 587.33, t0, 0.11, { gain: 0.095, attack: 0.004, type: "triangle" });
|
||||||
tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.028, type: "sine" });
|
tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.085, type: "sine" });
|
||||||
tone(ctx, master, 880, t0 + 0.12, 0.1, { gain: 0.022, release: 0.05 });
|
tone(ctx, master, 880, t0 + 0.12, 0.12, { gain: 0.075, release: 0.06 });
|
||||||
break;
|
break;
|
||||||
case "routine_start":
|
case "routine_start":
|
||||||
tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" });
|
tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" });
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { readOoSoundEnabled } from "./oo-sound-pref";
|
|||||||
|
|
||||||
export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm";
|
export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm";
|
||||||
|
|
||||||
const COOLDOWN_MIN_MS = 8_000;
|
const COOLDOWN_MIN_MS = 1_600;
|
||||||
const COOLDOWN_MAX_MS = 12_000;
|
const COOLDOWN_MAX_MS = 2_800;
|
||||||
|
|
||||||
let lastPlayedAt = 0;
|
let lastPlayedAt = 0;
|
||||||
let nextCooldownMs = COOLDOWN_MIN_MS;
|
let nextCooldownMs = COOLDOWN_MIN_MS;
|
||||||
@ -28,17 +28,23 @@ function vocalFormant(
|
|||||||
) {
|
) {
|
||||||
const osc = ctx.createOscillator();
|
const osc = ctx.createOscillator();
|
||||||
const gain = ctx.createGain();
|
const gain = ctx.createGain();
|
||||||
|
const boostedPeak = Math.min(0.22, gainPeak * 3.2);
|
||||||
|
osc.type = "sine";
|
||||||
|
osc.frequency.setValueAtTime(baseHz, startAt);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(baseHz * 1.12, startAt + duration * 0.75);
|
||||||
|
gain.gain.setValueAtTime(0.0001, startAt);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(boostedPeak, startAt + 0.02);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
|
||||||
|
if (typeof ctx.createBiquadFilter === "function") {
|
||||||
const filter = ctx.createBiquadFilter();
|
const filter = ctx.createBiquadFilter();
|
||||||
filter.type = "bandpass";
|
filter.type = "bandpass";
|
||||||
filter.frequency.value = baseHz * 2.4;
|
filter.frequency.value = baseHz * 1.6;
|
||||||
filter.Q.value = 4.5;
|
filter.Q.value = 1.8;
|
||||||
osc.type = "triangle";
|
|
||||||
osc.frequency.setValueAtTime(baseHz, startAt);
|
|
||||||
gain.gain.setValueAtTime(0.0001, startAt);
|
|
||||||
gain.gain.exponentialRampToValueAtTime(gainPeak, startAt + 0.025);
|
|
||||||
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
|
|
||||||
osc.connect(filter);
|
osc.connect(filter);
|
||||||
filter.connect(gain);
|
filter.connect(gain);
|
||||||
|
} else {
|
||||||
|
osc.connect(gain);
|
||||||
|
}
|
||||||
gain.connect(destination);
|
gain.connect(destination);
|
||||||
osc.start(startAt);
|
osc.start(startAt);
|
||||||
osc.stop(startAt + duration + 0.02);
|
osc.stop(startAt + duration + 0.02);
|
||||||
|
|||||||
@ -37,4 +37,10 @@ describe("queuedFollowUps", () => {
|
|||||||
const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z");
|
const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z");
|
||||||
expect(queuedFollowUps([synthetic], RUN_START, new Map())).toEqual([]);
|
expect(queuedFollowUps([synthetic], RUN_START, new Map())).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears queued state once sendNow starts a newer run after the message", () => {
|
||||||
|
const followUp = userMessage("queued-1", "2026-01-01T10:00:05.000Z");
|
||||||
|
expect(queuedFollowUps([followUp], RUN_START, new Map())).toEqual([followUp]);
|
||||||
|
expect(queuedFollowUps([followUp], "2026-01-01T10:00:05.001Z", new Map())).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,25 +1 @@
|
|||||||
import type { ThreadMessage } from "@rakazo/contracts";
|
export { queuedFollowUps } from "@rakazo/core";
|
||||||
|
|
||||||
/** The bot marks a steering message with this reaction once it has picked it up. */
|
|
||||||
const SEEN_REACTION = "👀";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User messages sent after the active run started that the bot has not picked
|
|
||||||
* up yet. The run's own trigger message predates the run, so it never counts.
|
|
||||||
*/
|
|
||||||
export function queuedFollowUps(
|
|
||||||
messages: readonly ThreadMessage[],
|
|
||||||
runCreatedAt: string | undefined,
|
|
||||||
reactions: ReadonlyMap<string, ReadonlyMap<string, number>>,
|
|
||||||
): ThreadMessage[] {
|
|
||||||
if (!runCreatedAt) return [];
|
|
||||||
const runStart = Date.parse(runCreatedAt);
|
|
||||||
if (Number.isNaN(runStart)) return [];
|
|
||||||
return messages.filter(
|
|
||||||
(message) =>
|
|
||||||
message.role === "user" &&
|
|
||||||
!message.id.includes(":") &&
|
|
||||||
Date.parse(message.createdAt) > runStart &&
|
|
||||||
!reactions.get(message.id)?.has(SEEN_REACTION),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -215,6 +215,53 @@ describe("Speaker", () => {
|
|||||||
expect(speaker.state.status).toBe("idle");
|
expect(speaker.state.status).toBe("idle");
|
||||||
expect(speaker.state.error).toBeUndefined();
|
expect(speaker.state.error).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("filters out novelty voices and selects a natural German voice for German text", async () => {
|
||||||
|
stubSelectedSpace("space-voice");
|
||||||
|
const speak = vi.fn();
|
||||||
|
const cancel = vi.fn();
|
||||||
|
const eddy = { name: "Eddy (German (Germany))", lang: "de-DE", localService: true };
|
||||||
|
const grandma = { name: "Grandma (German (Germany))", lang: "de-DE", localService: true };
|
||||||
|
const anna = { name: "Anna", lang: "de-DE", localService: true };
|
||||||
|
vi.stubGlobal(
|
||||||
|
"SpeechSynthesisUtterance",
|
||||||
|
class {
|
||||||
|
onend: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
voice: unknown = null;
|
||||||
|
lang = "";
|
||||||
|
rate = 1;
|
||||||
|
pitch = 1;
|
||||||
|
constructor(public text: string) {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
vi.stubGlobal("window", {
|
||||||
|
speechSynthesis: {
|
||||||
|
cancel,
|
||||||
|
getVoices: () => [eddy, grandma, anna],
|
||||||
|
speak,
|
||||||
|
},
|
||||||
|
localStorage: globalThis.localStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const speaker = new Speaker();
|
||||||
|
vi.spyOn(
|
||||||
|
speaker as unknown as { prepare: () => Promise<string[]> },
|
||||||
|
"prepare",
|
||||||
|
).mockRejectedValue(new Error("offline"));
|
||||||
|
|
||||||
|
const pending = speaker.speak("Ich habe das Projekt für dich aktualisiert.");
|
||||||
|
await vi.waitFor(() => expect(speak).toHaveBeenCalledOnce());
|
||||||
|
const utterance = speak.mock.calls[0]?.[0] as {
|
||||||
|
voice?: { name: string };
|
||||||
|
lang?: string;
|
||||||
|
onend?: () => void;
|
||||||
|
};
|
||||||
|
expect(utterance.lang).toBe("de-DE");
|
||||||
|
expect(utterance.voice?.name).toBe("Anna");
|
||||||
|
utterance.onend?.();
|
||||||
|
await pending;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("UtteranceStreamer", () => {
|
describe("UtteranceStreamer", () => {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core";
|
import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core";
|
||||||
|
import { ooAudioBus } from "./oo-audio-bus.js";
|
||||||
import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
||||||
|
import { readUiLocale } from "./ui-locale.js";
|
||||||
|
|
||||||
export type SpeechStatus = "idle" | "preparing" | "speaking";
|
export type SpeechStatus = "idle" | "preparing" | "speaking";
|
||||||
|
|
||||||
@ -76,7 +78,19 @@ export class Speaker {
|
|||||||
if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE);
|
if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bufferSource: AudioBufferSourceNode | null = null;
|
||||||
|
|
||||||
private teardownAudio() {
|
private teardownAudio() {
|
||||||
|
if (this.bufferSource) {
|
||||||
|
try {
|
||||||
|
this.bufferSource.onended = null;
|
||||||
|
this.bufferSource.stop();
|
||||||
|
this.bufferSource.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore already-stopped buffer source errors.
|
||||||
|
}
|
||||||
|
this.bufferSource = null;
|
||||||
|
}
|
||||||
if (this.audio) {
|
if (this.audio) {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
this.audio.src = "";
|
this.audio.src = "";
|
||||||
@ -225,14 +239,49 @@ export class Speaker {
|
|||||||
audio.onended = null;
|
audio.onended = null;
|
||||||
audio.onerror = null;
|
audio.onerror = null;
|
||||||
if (this.settlePlayback === done) this.settlePlayback = null;
|
if (this.settlePlayback === done) this.settlePlayback = null;
|
||||||
if (this.audio === audio) this.teardownAudio();
|
if (this.audio === audio || this.bufferSource) this.teardownAudio();
|
||||||
resolve(ok);
|
resolve(ok);
|
||||||
};
|
};
|
||||||
this.settlePlayback = done;
|
this.settlePlayback = done;
|
||||||
audio.onended = () => done(true);
|
audio.onended = () => done(true);
|
||||||
audio.onerror = () => done(false);
|
audio.onerror = () => {
|
||||||
audio.play().catch(() => done(false));
|
void this.playViaWebAudio(blob, live).then(done);
|
||||||
|
};
|
||||||
|
audio.play().catch(() => {
|
||||||
|
void this.playViaWebAudio(blob, live).then(done);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async playViaWebAudio(blob: Blob, live: () => boolean): Promise<boolean> {
|
||||||
|
if (!live()) return false;
|
||||||
|
const ctx = ooAudioBus.ensureContext();
|
||||||
|
if (
|
||||||
|
!ctx ||
|
||||||
|
typeof ctx.decodeAudioData !== "function" ||
|
||||||
|
typeof ctx.createBufferSource !== "function"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
if (!live()) return false;
|
||||||
|
const audioBuffer = await ctx.decodeAudioData(arrayBuffer.slice(0));
|
||||||
|
if (!live()) return false;
|
||||||
|
return await new Promise<boolean>((resolve) => {
|
||||||
|
const source = ctx.createBufferSource();
|
||||||
|
source.buffer = audioBuffer;
|
||||||
|
source.connect(ctx.destination);
|
||||||
|
this.bufferSource = source;
|
||||||
|
source.onended = () => {
|
||||||
|
if (this.bufferSource === source) this.bufferSource = null;
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
source.start(0);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryBrowserFallback(
|
async tryBrowserFallback(
|
||||||
@ -255,17 +304,65 @@ export class Speaker {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
window.speechSynthesis.cancel();
|
window.speechSynthesis.cancel();
|
||||||
|
window.speechSynthesis.resume?.();
|
||||||
const utterance = new SpeechSynthesisUtterance(text);
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
const voices = window.speechSynthesis.getVoices?.() ?? [];
|
const voices = window.speechSynthesis.getVoices?.() ?? [];
|
||||||
const isGerman = /[äöüß]|(?:und|der|die|das|ist|nicht|für|mit|den|dem|des)/i.test(text);
|
const uiLocale = readUiLocale();
|
||||||
const langPrefix = isGerman ? "de" : "en";
|
const voiceId = opts.voiceId ?? "";
|
||||||
|
const isGermanVoiceId = /^d[fm]_/i.test(voiceId);
|
||||||
|
const isGermanText =
|
||||||
|
/[äöüß]|(?:\b(?:und|der|die|das|ist|nicht|für|mit|den|dem|des|ein|eine|ich|wir|bitte)\b)/i.test(
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
const isGerman = isGermanVoiceId || uiLocale === "de" || isGermanText;
|
||||||
|
const langPrefix = isGerman ? "de" : uiLocale.split("-")[0] || "en";
|
||||||
|
utterance.lang = isGerman ? "de-DE" : langPrefix === "en" ? "en-US" : uiLocale;
|
||||||
|
|
||||||
|
const allLangVoices = voices.filter((v) =>
|
||||||
|
(v.lang ?? "").toLowerCase().replace(/_/g, "-").startsWith(langPrefix),
|
||||||
|
);
|
||||||
|
const noveltyHint =
|
||||||
|
/\b(?:eddy|flo|grandma|grandpa|reed|rocko|sandy|shelley|albert|bad news|bahh|bells|boing|bubbles|cellos|deranged|good news|hysterical|junior|organ|superstar|trinoids|whisper|zarvox|fred|kathy|ralph)\b/i;
|
||||||
|
const naturalLangVoices = allLangVoices.filter((v) => !noveltyHint.test(v.name ?? ""));
|
||||||
|
const langVoices = naturalLangVoices.length > 0 ? naturalLangVoices : allLangVoices;
|
||||||
|
const wantsMale = /^dm_/i.test(voiceId);
|
||||||
|
const wantsFemale = /^df_/i.test(voiceId) || (!wantsMale && isGerman);
|
||||||
|
const maleHint =
|
||||||
|
/yannick|martin|markus|stefan|viktor|hans|daniel|thomas|florian|conrad|male/i;
|
||||||
|
const femaleHint = /anna|petra|helena|marlene|vicki|katja|seraphina|amala|siri|female/i;
|
||||||
|
|
||||||
const matchedVoice =
|
const matchedVoice =
|
||||||
voices.find((v) => v.lang.startsWith(langPrefix) && !v.localService) ||
|
(wantsMale &&
|
||||||
voices.find((v) => v.lang.startsWith(langPrefix)) ||
|
(langVoices.find((v) => !v.localService && maleHint.test(v.name ?? "")) ||
|
||||||
|
langVoices.find((v) => maleHint.test(v.name ?? "")))) ||
|
||||||
|
(wantsFemale &&
|
||||||
|
(langVoices.find((v) => !v.localService && femaleHint.test(v.name ?? "")) ||
|
||||||
|
langVoices.find((v) => femaleHint.test(v.name ?? "")))) ||
|
||||||
|
langVoices.find((v) => !v.localService) ||
|
||||||
|
langVoices[0] ||
|
||||||
voices[0];
|
voices[0];
|
||||||
if (matchedVoice) utterance.voice = matchedVoice;
|
if (matchedVoice) utterance.voice = matchedVoice;
|
||||||
utterance.rate = 1.05;
|
|
||||||
|
// Distinct natural voice profiles for the 5 German presets when rendered via system TTS
|
||||||
|
if (voiceId === "dm_karlsson") {
|
||||||
|
utterance.rate = 0.98;
|
||||||
|
utterance.pitch = 0.85;
|
||||||
|
} else if (voiceId === "dm_thorsten") {
|
||||||
|
utterance.rate = 1.03;
|
||||||
|
utterance.pitch = 0.95;
|
||||||
|
} else if (voiceId === "df_ramona") {
|
||||||
|
utterance.rate = 0.97;
|
||||||
utterance.pitch = 1.0;
|
utterance.pitch = 1.0;
|
||||||
|
} else if (voiceId === "df_eva") {
|
||||||
|
utterance.rate = 1.02;
|
||||||
|
utterance.pitch = 1.04;
|
||||||
|
} else if (voiceId === "df_kerstin") {
|
||||||
|
utterance.rate = 1.05;
|
||||||
|
utterance.pitch = 1.08;
|
||||||
|
} else {
|
||||||
|
utterance.rate = 1.04;
|
||||||
|
utterance.pitch = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
status: "speaking",
|
status: "speaking",
|
||||||
|
|||||||
@ -100,3 +100,37 @@ export async function disableWebPush(): Promise<void> {
|
|||||||
await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint });
|
await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint });
|
||||||
await subscription.unsubscribe().catch(() => undefined);
|
await subscription.unsubscribe().catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True on iOS/iPadOS when opened in a browser tab rather than as an installed Home-Screen PWA
|
||||||
|
* (where Apple WebKit only exposes Web Push in standalone display mode).
|
||||||
|
*/
|
||||||
|
export function isIosBrowserNeedsHomeScreen(): boolean {
|
||||||
|
if (typeof window === "undefined" || typeof navigator === "undefined" || desktopBridge()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ua = navigator.userAgent || "";
|
||||||
|
const isIos =
|
||||||
|
/iPad|iPhone|iPod/i.test(ua) ||
|
||||||
|
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||||
|
if (!isIos) return false;
|
||||||
|
const standalone =
|
||||||
|
(typeof window.matchMedia === "function" &&
|
||||||
|
window.matchMedia("(display-mode: standalone)").matches) ||
|
||||||
|
Boolean((navigator as Navigator & { standalone?: boolean }).standalone);
|
||||||
|
return !standalone && !webPushSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Syncs the installed PWA / OS app icon badge with the actionable "Needs You" count. */
|
||||||
|
export function syncAppBadge(count: number): void {
|
||||||
|
if (typeof navigator === "undefined") return;
|
||||||
|
const nav = navigator as Navigator & {
|
||||||
|
setAppBadge?: (contents?: number) => Promise<void>;
|
||||||
|
clearAppBadge?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
if (count > 0 && typeof nav.setAppBadge === "function") {
|
||||||
|
void nav.setAppBadge(count).catch(() => undefined);
|
||||||
|
} else if (count <= 0 && typeof nav.clearAppBadge === "function") {
|
||||||
|
void nav.clearAppBadge().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr "Browser von {0}"
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr "{0}s Browser erscheint hier, wenn der Computer läuft."
|
msgstr "{0}s Browser erscheint hier, wenn der Computer läuft."
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr "{0}s Bildschirm"
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr "{actionCount, plural, one {# Sache braucht dich} other {# Sachen brauchen dich}}"
|
msgstr "{actionCount, plural, one {# Sache braucht dich} other {# Sachen brauchen dich}}"
|
||||||
@ -182,10 +177,38 @@ msgstr "{botName} prüft die Architektur…"
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr "{botName} räumt den Weg frei…"
|
msgstr "{botName} räumt den Weg frei…"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr "{botName} baut & tüftelt"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr "{botName} prüft ein Problem"
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr "{botName} ist dran…"
|
msgstr "{botName} ist dran…"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr "{botName} antwortet"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr "{botName} liest & recherchiert"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr "{botName} denkt nach"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr "{botName} wartet auf deine Antwort"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr "{botName} arbeitet"
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "Noch {remaining} · Bot beobachtet nur"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}"
|
msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}"
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr "{toolCount, plural, one {# Tool} other {# Tools}}"
|
msgstr "{toolCount, plural, one {# Tool} other {# Tools}}"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} arbeitet"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -433,7 +453,7 @@ msgstr "Über Gruppen"
|
|||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About spaces"
|
msgid "About spaces"
|
||||||
msgstr "Über Spaces"
|
msgstr "Über Projekte"
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "About you"
|
msgid "About you"
|
||||||
@ -544,11 +564,16 @@ msgstr "Weiteres hinzufügen"
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr "Als Projekte hinzufügen"
|
msgstr "Als Projekte hinzufügen"
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr "Füge evOo über „Teilen“ zum Home-Bildschirm hinzu, um Push-Mitteilungen auf iOS zu aktivieren."
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Executor hinzufügen"
|
msgstr "Executor hinzufügen"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr "Ziel hinzufügen"
|
msgstr "Ziel hinzufügen"
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Immer erlaubt"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr "Analysiere das Projekt und gib mir einen kurzen Überblick."
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr "Projekt wird analysiert…"
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Antwort"
|
msgstr "Antwort"
|
||||||
@ -996,7 +1029,7 @@ msgstr "Körper"
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Live-Desktop wird gestartet…"
|
msgstr "Live-Desktop wird gestartet…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "Computer von {0} wird gestartet"
|
msgstr "Computer von {0} wird gestartet"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Bot-Bildschirm"
|
msgstr "Bot-Bildschirm"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Bot-Bildschirmvorschau"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "Zu verknüpfender Bot"
|
msgstr "Zu verknüpfender Bot"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "Zu verknüpfender Bot"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Bots handeln standardmäßig ohne Nachfrage. Füge nur dann eine Ausnahme hinzu, wenn du eine Aktionsart zuerst prüfen möchtest."
|
msgstr "Bots handeln standardmäßig ohne Nachfrage. Füge nur dann eine Ausnahme hinzu, wenn du eine Aktionsart zuerst prüfen möchtest."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Bots arbeiten"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr "Schleife"
|
msgstr "Schleife"
|
||||||
@ -1064,7 +1089,6 @@ msgstr "Durchsuchen"
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr "MCP-Server durchsuchen"
|
msgstr "MCP-Server durchsuchen"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr "Browser"
|
msgstr "Browser"
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Computer"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen."
|
msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr "Richte auf dem Server einen Plugin-Katalog ein, um Apps zu verbinden."
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Durch die Bereitstellung konfiguriert"
|
msgstr "Durch die Bereitstellung konfiguriert"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Verbunden, {0} wird verwendet."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr "Verbundene App"
|
msgstr "Verbundene App"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr "Verbundene Tools"
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar."
|
msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar."
|
||||||
@ -2077,7 +2109,7 @@ msgstr "Neue Gruppe erstellen"
|
|||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "Create new Space"
|
msgid "Create new Space"
|
||||||
msgstr "Neuen Space erstellen"
|
msgstr "Neues Projekt erstellen"
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Create project"
|
msgid "Create project"
|
||||||
@ -2086,7 +2118,7 @@ msgstr "Projekt erstellen"
|
|||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Create space"
|
msgid "Create space"
|
||||||
msgstr "Space erstellen"
|
msgstr "Projekt erstellen"
|
||||||
|
|
||||||
#: src/components/integrations/GithubConnect.tsx
|
#: src/components/integrations/GithubConnect.tsx
|
||||||
msgid "Create token on GitHub"
|
msgid "Create token on GitHub"
|
||||||
@ -2241,7 +2273,7 @@ msgstr "Erinnerungen ebenfalls löschen"
|
|||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Delete space"
|
msgid "Delete space"
|
||||||
msgstr "Space löschen"
|
msgstr "Projekt löschen"
|
||||||
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Delete your account?"
|
msgid "Delete your account?"
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Verbindung wird getrennt…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr "Ausblenden"
|
msgstr "Ausblenden"
|
||||||
@ -2381,7 +2414,9 @@ msgstr "Kein Stress — wir sind jetzt da."
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Fertig"
|
msgstr "Fertig"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr "Erledigt"
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr "Punkt"
|
msgstr "Punkt"
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "{0} herunterladen"
|
msgstr "{0} herunterladen"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "{0} herunterladen"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "{name} herunterladen"
|
msgstr "{name} herunterladen"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Erweitern"
|
msgstr "Erweitern"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "{0} ausklappen"
|
msgstr "{0} ausklappen"
|
||||||
@ -2660,6 +2699,10 @@ msgstr "Workspace-Dateien konnten nicht geladen werden"
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr "Datei konnte nicht geöffnet werden"
|
msgstr "Datei konnte nicht geöffnet werden"
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr "Nachricht konnte nicht entfernt werden"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "Nachricht konnte nicht gesendet werden"
|
msgstr "Nachricht konnte nicht gesendet werden"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr "Das sehe ich in den Logs…"
|
msgstr "Das sehe ich in den Logs…"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Hallo, so klinge ich, wenn ich Antworten vorlese."
|
msgstr "Hallo, so klinge ich, wenn wir miteinander sprechen."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr "Bots ausblenden"
|
msgstr "Bots ausblenden"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr "Details ausblenden"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "Passwort verbergen"
|
msgstr "Passwort verbergen"
|
||||||
@ -3080,6 +3128,7 @@ msgstr "Bot importieren"
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "OpenAPI-JSON importieren"
|
msgstr "OpenAPI-JSON importieren"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr "In Bearbeitung"
|
msgstr "In Bearbeitung"
|
||||||
@ -3166,6 +3215,10 @@ msgstr "Isoliert"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Seine Unterhaltung, Dateien und Routinen werden dauerhaft gelöscht. Von ihm erstellte Bots bleiben in deiner Liste."
|
msgstr "Seine Unterhaltung, Dateien und Routinen werden dauerhaft gelöscht. Von ihm erstellte Bots bleiben in deiner Liste."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr "dazugeschaltet"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "Zur neuesten Nachricht springen"
|
msgstr "Zur neuesten Nachricht springen"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "Zur neuesten Nachricht springen"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Zur beantworteten Nachricht springen"
|
msgstr "Zur beantworteten Nachricht springen"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr "Sag einfach im Chat Bescheid, wenn etwas regelmäßig erledigt werden soll."
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3771,9 +3828,10 @@ msgstr "Neuer Skill"
|
|||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "New space"
|
msgid "New space"
|
||||||
msgstr "Neuer Space"
|
msgstr "Neues Projekt"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "Weiter"
|
msgstr "Weiter"
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr "Noch keine Projekte."
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "Keine Anbieter gefunden."
|
msgstr "Keine Anbieter gefunden."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr "Keine letzte Aktivität"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr "Keine Remote-MCP-Server gefunden"
|
msgstr "Keine Remote-MCP-Server gefunden"
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "Keine Ergebnisse"
|
msgstr "Keine Ergebnisse"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "Noch keine Ausführungen"
|
msgstr "Noch keine Ausführungen"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Jetzt"
|
msgstr "Jetzt"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr "Name des Oo"
|
msgstr "Name des Oo"
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Öffnen"
|
msgstr "Öffnen"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr "Unterhaltung öffnen"
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr "Im Browser öffnen"
|
msgstr "Im Browser öffnen"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "In vollständigem Fenster öffnen"
|
msgstr "In vollständigem Fenster öffnen"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr "Privater Arbeitsbereich mit eigenen Bots und Gruppen."
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr "Projekt"
|
msgstr "Projekt"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr "Projekt · Sessions & Ordner"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr "Lese den Stacktrace…"
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Bereit"
|
msgstr "Bereit"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "Bereit"
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "Denkaufwand"
|
msgstr "Denkaufwand"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr "Nur abrufen"
|
msgstr "Nur abrufen"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Kürzlich"
|
msgstr "Kürzlich"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "{0} entfernen"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr "Ordner entfernen"
|
msgstr "Ordner entfernen"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr "Aus der Warteschlange entfernen"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr "Git-Ereignis entfernen"
|
msgstr "Git-Ereignis entfernen"
|
||||||
@ -4653,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr "Dein Arbeitsbereich wird wiederhergestellt"
|
msgstr "Dein Arbeitsbereich wird wiederhergestellt"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr "Fortsetzen"
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Wiederholen"
|
msgstr "Wiederholen"
|
||||||
@ -4700,11 +4773,13 @@ msgstr "Rund"
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Routine"
|
msgstr "Routine"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Routinen"
|
msgstr "Routinen"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "Ausführen um"
|
msgstr "Ausführen um"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "Ausführungsverlauf"
|
msgstr "Ausführungsverlauf"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Wird ausgeführt…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr "Läuft, wenn dieser Bot eine verifizierte Nachricht von diesem Anbieter erhält."
|
msgstr "Läuft, wenn dieser Bot eine verifizierte Nachricht von diesem Anbieter erhält."
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr "Sandbox"
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Antwort senden"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Senden"
|
msgstr "Senden"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Jetzt gleich senden"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Link zum Zurücksetzen senden"
|
msgstr "Link zum Zurücksetzen senden"
|
||||||
@ -5021,6 +5097,10 @@ msgstr "Bots anzeigen"
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr "Computer anzeigen"
|
msgstr "Computer anzeigen"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr "Details anzeigen"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr "Weniger anzeigen"
|
msgstr "Weniger anzeigen"
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Mehr anzeigen"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "Passwort anzeigen"
|
msgstr "Passwort anzeigen"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Einstellungen anzeigen"
|
msgstr "Einstellungen anzeigen"
|
||||||
@ -5136,7 +5217,7 @@ msgstr "Space-Standard"
|
|||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Spaces"
|
msgid "Spaces"
|
||||||
msgstr "Spaces"
|
msgstr "Projekte"
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Sparkle"
|
msgid "Sparkle"
|
||||||
@ -5172,6 +5253,7 @@ msgstr "Vorlesen an oder aus"
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr "Spreche mit Bedacht…"
|
msgstr "Spreche mit Bedacht…"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr "Spezialisten-Team"
|
msgstr "Spezialisten-Team"
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Anlernen beenden"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr "Timer stoppen"
|
msgstr "Timer stoppen"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr "Gestoppt"
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Aufgabe wird angelernt. Beende das Anlernen, bevor du eine neue Nachrich
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Team"
|
msgstr "Team"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "Diese Abonnementanmeldung ist in Rakazo noch nicht verfügbar. Verwende
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht."
|
msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr "Unterhaltungen nach Thema · Ordner"
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr "Bis wann?"
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "Aktuell"
|
msgstr "Aktuell"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr "Arbeitskanal offen — leg los."
|
msgstr "Arbeitskanal offen — leg los."
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr "Arbeitet"
|
msgstr "Arbeitet"
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Gestern"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "Du"
|
msgstr "Du"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "Dein Name"
|
msgstr "Dein Name"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr "Wenn das an ist, liest dein Oo Antworten vor."
|
msgstr "Wenn das an ist, spricht dein Oo ganz natürlich mit dir."
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern."
|
msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr "Dein Profil"
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden."
|
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden."
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr "Vergrößern"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr "Verkleinern"
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr "{0}'s browser"
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr "{0}'s browser appears here when the computer is running."
|
msgstr "{0}'s browser appears here when the computer is running."
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr "{0}'s screen"
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgstr "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
@ -182,10 +177,38 @@ msgstr "{botName} checks the architecture…"
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr "{botName} clears the path…"
|
msgstr "{botName} clears the path…"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr "{botName} is building"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr "{botName} is checking a problem"
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr "{botName} is on it…"
|
msgstr "{botName} is on it…"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr "{botName} is replying"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr "{botName} is researching"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr "{botName} is thinking"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr "{botName} is waiting for your answer"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr "{botName} is working"
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "{remaining} left · bot is watching, not acting"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr "{stepCount, plural, one {# step} other {# steps}}"
|
msgstr "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr "{toolCount, plural, one {# tool} other {# tools}}"
|
msgstr "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} is working"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr "Add another"
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr "Add as projects"
|
msgstr "Add as projects"
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Add Executor"
|
msgstr "Add Executor"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr "Add goal"
|
msgstr "Add goal"
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Always allowed"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr "Analyze the project and give me a brief overview."
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr "Analyzing project…"
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Answer"
|
msgstr "Answer"
|
||||||
@ -996,7 +1029,7 @@ msgstr "Body"
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Booting live desktop…"
|
msgstr "Booting live desktop…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "Booting up {0}’s computer"
|
msgstr "Booting up {0}’s computer"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Bot screen"
|
msgstr "Bot screen"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Bot screen preview"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "Bot to link"
|
msgstr "Bot to link"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "Bot to link"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgstr "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Bots are working"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr "Bow"
|
msgstr "Bow"
|
||||||
@ -1064,7 +1089,6 @@ msgstr "Browse"
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr "Browse MCP servers"
|
msgstr "Browse MCP servers"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr "Browser"
|
msgstr "Browser"
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Computers"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr "Configure a plugin catalog on the server to connect apps."
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Configured by deployment"
|
msgstr "Configured by deployment"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Connected and using {0}."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr "Connected app"
|
msgstr "Connected app"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr "Connected tools"
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Connected. Its tools are available from your next message."
|
msgstr "Connected. Its tools are available from your next message."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Disconnecting…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr "Dismiss"
|
msgstr "Dismiss"
|
||||||
@ -2381,7 +2414,9 @@ msgstr "Don't stress — we're here now."
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Done"
|
msgstr "Done"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr "Done"
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr "Dot"
|
msgstr "Dot"
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "Download {0}"
|
msgstr "Download {0}"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "Download {0}"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "Download {name}"
|
msgstr "Download {name}"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Expand"
|
msgstr "Expand"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "Expand {0}"
|
msgstr "Expand {0}"
|
||||||
@ -2660,6 +2699,10 @@ msgstr "Failed to load workspace files"
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr "Failed to open file"
|
msgstr "Failed to open file"
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr "Failed to remove message"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "Failed to send message"
|
msgstr "Failed to send message"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr "Here's what I see in the logs…"
|
msgstr "Here's what I see in the logs…"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Hi, this is how I'll sound when I read replies out loud."
|
msgstr "Hi, this is how I'll sound when we talk."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr "Hide bots"
|
msgstr "Hide bots"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr "Hide details"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "Hide password"
|
msgstr "Hide password"
|
||||||
@ -3080,6 +3128,7 @@ msgstr "Import bot"
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "Import OpenAPI JSON"
|
msgstr "Import OpenAPI JSON"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr "In Bearbeitung"
|
msgstr "In Bearbeitung"
|
||||||
@ -3166,6 +3215,10 @@ msgstr "Isolated"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgstr "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr "joined"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "Jump to latest"
|
msgstr "Jump to latest"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "Jump to latest"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Jump to replied message"
|
msgstr "Jump to replied message"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr "Just ask in the chat whenever something should happen regularly."
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr "New space"
|
msgstr "New space"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "Next"
|
msgstr "Next"
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr "No projects yet."
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "No providers found."
|
msgstr "No providers found."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr "No recent activity"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr "No remote MCP servers found"
|
msgstr "No remote MCP servers found"
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "No results"
|
msgstr "No results"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "No runs yet"
|
msgstr "No runs yet"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Notifications"
|
msgstr "Notifications"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Now"
|
msgstr "Now"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr "Oo name"
|
msgstr "Oo name"
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Open"
|
msgstr "Open"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr "Open conversation"
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr "Open in browser"
|
msgstr "Open in browser"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "Open in full window"
|
msgstr "Open in full window"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr "Private workspace with its own bots and groups."
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr "Project"
|
msgstr "Project"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr "Project · sessions & folders"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr "Reading the stack trace…"
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Ready"
|
msgstr "Ready"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "Ready"
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "Reasoning effort"
|
msgstr "Reasoning effort"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr "Recall only"
|
msgstr "Recall only"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Recent"
|
msgstr "Recent"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "Remove {0}"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr "Remove folder"
|
msgstr "Remove folder"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr "Remove from queue"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr "Remove Git event"
|
msgstr "Remove Git event"
|
||||||
@ -4653,6 +4721,11 @@ msgstr "Restore the last saved workspace. Unsaved work on the computer is lost."
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr "Restoring your workspace"
|
msgstr "Restoring your workspace"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr "Resume"
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Retry"
|
msgstr "Retry"
|
||||||
@ -4700,11 +4773,13 @@ msgstr "Round"
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Routine"
|
msgstr "Routine"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Routines"
|
msgstr "Routines"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "Run at"
|
msgstr "Run at"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "Run history"
|
msgstr "Run history"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Running…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr "Runs when this bot receives a verified message from this provider."
|
msgstr "Runs when this bot receives a verified message from this provider."
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr "Sandbox"
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Send answer"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Send it"
|
msgstr "Send it"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Send now"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Send reset link"
|
msgstr "Send reset link"
|
||||||
@ -5021,6 +5097,10 @@ msgstr "Show bots"
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr "Show computer"
|
msgstr "Show computer"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr "Show details"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr "Show less"
|
msgstr "Show less"
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Show more"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "Show password"
|
msgstr "Show password"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Show settings"
|
msgstr "Show settings"
|
||||||
@ -5172,6 +5253,7 @@ msgstr "Speaking on or off"
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr "Speaking with intent…"
|
msgstr "Speaking with intent…"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr "Specialist crew"
|
msgstr "Specialist crew"
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Stop teaching"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr "Stop timer"
|
msgstr "Stop timer"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr "Stopped"
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Teaching in progress. Stop teaching before sending a new message."
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Team"
|
msgstr "Team"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "This subscription sign-in is not available in Rakazo yet. Use a deployme
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr "This will permanently delete this project and all its conversations, bots, and memory."
|
msgstr "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr "Threads by topic · folders"
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr "Until when?"
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "Up to date"
|
msgstr "Up to date"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr "Work channel open — go ahead."
|
msgstr "Work channel open — go ahead."
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr "Working"
|
msgstr "Working"
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Yesterday"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "You"
|
msgstr "You"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "Your name"
|
msgstr "Your name"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr "Your Oo reads replies aloud when this is on."
|
msgstr "Your Oo talks with you naturally when this is on."
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr "Your Oo will communicate in this language. You can change it later."
|
msgstr "Your Oo will communicate in this language. You can change it later."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr "Your profile"
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr "Your specialists. One calm place to talk."
|
msgstr "Your specialists. One calm place to talk."
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr "Zoom in"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr "Zoom out"
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr "Pantalla de {0}"
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "Quedan {remaining} · el bot observa, no actúa"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} está trabajando"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Agregar Executor"
|
msgstr "Agregar Executor"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Siempre permitido"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Responder"
|
msgstr "Responder"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Iniciando escritorio en vivo…"
|
msgstr "Iniciando escritorio en vivo…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "Iniciando la computadora de {0}"
|
msgstr "Iniciando la computadora de {0}"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Pantalla del bot"
|
msgstr "Pantalla del bot"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Vista previa de la pantalla del bot"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "Bot a vincular"
|
msgstr "Bot a vincular"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "Bot a vincular"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Los bots actúan sin preguntar de forma predeterminada. Agrega una excepción solo cuando quieras revisar primero un tipo de acción."
|
msgstr "Los bots actúan sin preguntar de forma predeterminada. Agrega una excepción solo cuando quieras revisar primero un tipo de acción."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Los bots están trabajando"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Computadoras"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env."
|
msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Configurado por el despliegue"
|
msgstr "Configurado por el despliegue"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Conectado y usando {0}."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
|
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Desconectando…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr "Descartar"
|
msgstr "Descartar"
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Listo"
|
msgstr "Listo"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "Descargar {0}"
|
msgstr "Descargar {0}"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "Descargar {0}"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "Descargar {name}"
|
msgstr "Descargar {name}"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Expandir"
|
msgstr "Expandir"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "Expandir {0}"
|
msgstr "Expandir {0}"
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "No se pudo enviar el mensaje"
|
msgstr "No se pudo enviar el mensaje"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Hola, así sonaré cuando lea las respuestas en voz alta."
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "Ocultar contraseña"
|
msgstr "Ocultar contraseña"
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "Importar JSON de OpenAPI"
|
msgstr "Importar JSON de OpenAPI"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "Aislado"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Su conversación, archivos y rutinas se eliminarán de forma permanente. Los bots que creó permanecerán en tu lista."
|
msgstr "Su conversación, archivos y rutinas se eliminarán de forma permanente. Los bots que creó permanecerán en tu lista."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "Ir al más reciente"
|
msgstr "Ir al más reciente"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "Ir al más reciente"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Ir al mensaje respondido"
|
msgstr "Ir al mensaje respondido"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr "Nuevo space"
|
msgstr "Nuevo space"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "No se encontraron proveedores."
|
msgstr "No se encontraron proveedores."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "Sin resultados"
|
msgstr "Sin resultados"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "Aún no hay ejecuciones"
|
msgstr "Aún no hay ejecuciones"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Notificaciones"
|
msgstr "Notificaciones"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Ahora"
|
msgstr "Ahora"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Abrir"
|
msgstr "Abrir"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "Abrir en ventana completa"
|
msgstr "Abrir en ventana completa"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "Esfuerzo de razonamiento"
|
msgstr "Esfuerzo de razonamiento"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Recientes"
|
msgstr "Recientes"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "Quitar {0}"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Rutina"
|
msgstr "Rutina"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Rutinas"
|
msgstr "Rutinas"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "Ejecutar a las"
|
msgstr "Ejecutar a las"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "Historial de ejecuciones"
|
msgstr "Historial de ejecuciones"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Ejecutando…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Enviar respuesta"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Enviarlo"
|
msgstr "Enviarlo"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Enviar ahora"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Enviar enlace de restablecimiento"
|
msgstr "Enviar enlace de restablecimiento"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Mostrar más"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "Mostrar contraseña"
|
msgstr "Mostrar contraseña"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Mostrar configuración"
|
msgstr "Mostrar configuración"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Detener enseñanza"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Enseñanza en curso: detén la enseñanza antes de enviar un nuevo mensa
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Equipo"
|
msgstr "Equipo"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "Este inicio de sesión por suscripción aún no está disponible en Raka
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "Actualizado"
|
msgstr "Actualizado"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Ayer"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "Tú"
|
msgstr "Tú"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "Tu nombre"
|
msgstr "Tu nombre"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "{remaining} शेष · बॉट देख रहा है, कार
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} काम कर रहा है"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Executor जोड़ें"
|
msgstr "Executor जोड़ें"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "हमेशा अनुमत"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "उत्तर"
|
msgstr "उत्तर"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "लाइव डेस्कटॉप शुरू हो रहा है…"
|
msgstr "लाइव डेस्कटॉप शुरू हो रहा है…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "{0} का कंप्यूटर शुरू हो रहा है"
|
msgstr "{0} का कंप्यूटर शुरू हो रहा है"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "बॉट"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "बॉट स्क्रीन"
|
msgstr "बॉट स्क्रीन"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "बॉट स्क्रीन प्रीव्यू"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1031,10 +1060,6 @@ msgstr ""
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।"
|
msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "बॉट काम कर रहे हैं"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "कंप्यूटर"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।"
|
msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।"
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर"
|
msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "{0} कनेक्ट किया गया और उपयोग म
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।"
|
msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।"
|
||||||
@ -2322,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "पूर्ण"
|
msgstr "पूर्ण"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "{0} डाउनलोड करें"
|
msgstr "{0} डाउनलोड करें"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "{0} डाउनलोड करें"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "{name} डाउनलोड करें"
|
msgstr "{name} डाउनलोड करें"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "विस्तृत करें"
|
msgstr "विस्तृत करें"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "संदेश भेजने में विफल"
|
msgstr "संदेश भेजने में विफल"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "नमस्ते, उत्तर पढ़कर सुनाते समय मेरी आवाज़ ऐसी होगी।"
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "OpenAPI JSON इम्पोर्ट करें"
|
msgstr "OpenAPI JSON इम्पोर्ट करें"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "पृथक"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
|
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "नवीनतम संदेश पर जाएँ"
|
msgstr "नवीनतम संदेश पर जाएँ"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "नवीनतम संदेश पर जाएँ"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "जिस संदेश का उत्तर दिया गया उस पर जाएं"
|
msgstr "जिस संदेश का उत्तर दिया गया उस पर जाएं"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "कोई प्रोवाइडर नहीं मिला।"
|
msgstr "कोई प्रोवाइडर नहीं मिला।"
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "कोई परिणाम नहीं"
|
msgstr "कोई परिणाम नहीं"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "सूचनाएं"
|
msgstr "सूचनाएं"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "अभी"
|
msgstr "अभी"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "खोलें"
|
msgstr "खोलें"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "पूरी विंडो में खोलें"
|
msgstr "पूरी विंडो में खोलें"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "रीज़निंग प्रयास"
|
msgstr "रीज़निंग प्रयास"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "हाल के"
|
msgstr "हाल के"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "{0} हटाएं"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "रूटीन"
|
msgstr "रूटीन"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "रूटीन्स"
|
msgstr "रूटीन्स"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "चल रहा है…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "उत्तर भेजें"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "इसे भेजें"
|
msgstr "इसे भेजें"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "अभी भेजें"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "रीसेट लिंक भेजें"
|
msgstr "रीसेट लिंक भेजें"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "और दिखाएँ"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "सेटिंग्स दिखाएं"
|
msgstr "सेटिंग्स दिखाएं"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "सिखाना रोकें"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "सिखाना जारी है। नया संदेश भ
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "टीम"
|
msgstr "टीम"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "यह सब्सक्रिप्शन साइन-इन अभ
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "कल"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "आप"
|
msgstr "आप"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "आपका नाम"
|
msgstr "आपका नाम"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr "{0}의 화면"
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "남은 시간 {remaining} · Bot은 화면만 보고 직접 조작하지
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} 작업 중"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Executor 추가"
|
msgstr "Executor 추가"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "항상 허용됨"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "답변"
|
msgstr "답변"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "라이브 데스크톱 시작 중…"
|
msgstr "라이브 데스크톱 시작 중…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "{0}의 컴퓨터를 켜는 중"
|
msgstr "{0}의 컴퓨터를 켜는 중"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "봇"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Bot 화면"
|
msgstr "Bot 화면"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Bot 화면 미리보기"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "연결할 Bot"
|
msgstr "연결할 Bot"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "연결할 Bot"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요."
|
msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "봇 작업 중"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "컴퓨터"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요."
|
msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "서버에서 설정됨"
|
msgstr "서버에서 설정됨"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
|
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "연결 해제 중…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr "닫기"
|
msgstr "닫기"
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "완료"
|
msgstr "완료"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "{0} 다운로드"
|
msgstr "{0} 다운로드"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "{0} 다운로드"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "{name} 다운로드"
|
msgstr "{name} 다운로드"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "펼치기"
|
msgstr "펼치기"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "{0} 펼치기"
|
msgstr "{0} 펼치기"
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "메시지를 보내지 못했습니다."
|
msgstr "메시지를 보내지 못했습니다."
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "안녕하세요. 답변을 소리 내어 읽을 때 이런 목소리로 들립니다."
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "비밀번호 숨기기"
|
msgstr "비밀번호 숨기기"
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "OpenAPI JSON 가져오기"
|
msgstr "OpenAPI JSON 가져오기"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "이 Bot만 사용"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
|
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "최신 메시지로 이동"
|
msgstr "최신 메시지로 이동"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "최신 메시지로 이동"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "답장한 메시지로 이동"
|
msgstr "답장한 메시지로 이동"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr "새 스페이스"
|
msgstr "새 스페이스"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "검색된 AI 서비스가 없습니다."
|
msgstr "검색된 AI 서비스가 없습니다."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "검색 결과가 없습니다"
|
msgstr "검색 결과가 없습니다"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "아직 실행 기록이 없습니다"
|
msgstr "아직 실행 기록이 없습니다"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "알림"
|
msgstr "알림"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "지금"
|
msgstr "지금"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "열기"
|
msgstr "열기"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "전체 창으로 열기"
|
msgstr "전체 창으로 열기"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "추론 강도"
|
msgstr "추론 강도"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "최근"
|
msgstr "최근"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "{0} 삭제"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "자동 실행"
|
msgstr "자동 실행"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "자동 실행"
|
msgstr "자동 실행"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "실행 시간"
|
msgstr "실행 시간"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "실행 기록"
|
msgstr "실행 기록"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "실행 중…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "답변 보내기"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "보내기"
|
msgstr "보내기"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "지금 보내기"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "재설정 링크 보내기"
|
msgstr "재설정 링크 보내기"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "더 보기"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "비밀번호 표시"
|
msgstr "비밀번호 표시"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "설정 보기"
|
msgstr "설정 보기"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "가르치기 종료"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "작업을 가르치는 중입니다. 새 메시지를 보내려면 먼
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "팀 공용"
|
msgstr "팀 공용"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "최신 상태"
|
msgstr "최신 상태"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "어제"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "나"
|
msgstr "나"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "이름"
|
msgstr "이름"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "{remaining} restantes · o bot está observando, não agindo"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} está trabalhando"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Adicionar Executor"
|
msgstr "Adicionar Executor"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Sempre permitido"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Resposta"
|
msgstr "Resposta"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Iniciando a área de trabalho ao vivo..."
|
msgstr "Iniciando a área de trabalho ao vivo..."
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "Inicializando o computador do {0}"
|
msgstr "Inicializando o computador do {0}"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Tela do bot"
|
msgstr "Tela do bot"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Pré-visualização da tela"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1031,10 +1060,6 @@ msgstr ""
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Os bots agem sem perguntar por padrão. Adicione uma exceção apenas quando quiser analisar um tipo de ação primeiro."
|
msgstr "Os bots agem sem perguntar por padrão. Adicione uma exceção apenas quando quiser analisar um tipo de ação primeiro."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Bots estão trabalhando"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Computadores"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env."
|
msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Configurado por implantação"
|
msgstr "Configurado por implantação"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Conectado e usando {0}."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem."
|
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Desconectando…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Concluído"
|
msgstr "Concluído"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "Baixar {0}"
|
msgstr "Baixar {0}"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "Baixar {0}"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "Baixar {name}"
|
msgstr "Baixar {name}"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Expandir"
|
msgstr "Expandir"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "Ocorreu um erro ao enviar a mensagem"
|
msgstr "Ocorreu um erro ao enviar a mensagem"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Olá, é assim que vou soar quando ler as respostas em voz alta."
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "Importar OpenAPI JSON"
|
msgstr "Importar OpenAPI JSON"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "Isolado"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Suas conversas, arquivos e rotinas serão excluídos permanentemente. Os bots criados ficam na sua lista."
|
msgstr "Suas conversas, arquivos e rotinas serão excluídos permanentemente. Os bots criados ficam na sua lista."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "Ir para a mensagem mais recente"
|
msgstr "Ir para a mensagem mais recente"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "Ir para a mensagem mais recente"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Ir para a mensagem respondida"
|
msgstr "Ir para a mensagem respondida"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "Nenhum provedor encontrado"
|
msgstr "Nenhum provedor encontrado"
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "Nenhum resultado"
|
msgstr "Nenhum resultado"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Notificações"
|
msgstr "Notificações"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Agora"
|
msgstr "Agora"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Abrir"
|
msgstr "Abrir"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "Abrir em janela cheia"
|
msgstr "Abrir em janela cheia"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "Esforço de raciocínio"
|
msgstr "Esforço de raciocínio"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Recente"
|
msgstr "Recente"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "Remover {0}"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr "Restaure o último espaço de trabalho salvo. O trabalho não salvo no c
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Rotina"
|
msgstr "Rotina"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Rotinas"
|
msgstr "Rotinas"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Executando..."
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Enviar resposta"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Enviar"
|
msgstr "Enviar"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Enviar agora"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Enviar link de redefinição"
|
msgstr "Enviar link de redefinição"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Mostrar mais"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Mostrar configurações"
|
msgstr "Mostrar configurações"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Parar de ensinar"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Ensino em andamento. Pare de ensinar antes de enviar uma nova mensagem."
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Equipe"
|
msgstr "Equipe"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "Este login de assinatura ainda não está disponível no Rakazo. Use uma
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Ontem"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "Você"
|
msgstr "Você"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "O seu nome"
|
msgstr "O seu nome"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr "Экран {0}"
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "Осталось: {remaining} · бот наблюдает, но не д
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} Б"
|
msgstr "{size} Б"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr "{toolCount, plural, one {# инструмент} few {# инструмента} many {# инструментов} other {# инструмента}}"
|
msgstr "{toolCount, plural, one {# инструмент} few {# инструмента} many {# инструментов} other {# инструмента}}"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} работает"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr "Добавить еще"
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Добавить Executor"
|
msgstr "Добавить Executor"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Всегда разрешено"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Ответ"
|
msgstr "Ответ"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Загрузка рабочего стола…"
|
msgstr "Загрузка рабочего стола…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "Загрузка компьютера {0}"
|
msgstr "Загрузка компьютера {0}"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Бот"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Экран бота"
|
msgstr "Экран бота"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Предварительный просмотр экрана бота"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "Бот для ссылки"
|
msgstr "Бот для ссылки"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "Бот для ссылки"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия."
|
msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Боты работают"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr "Просмотреть MCP-серверы"
|
msgstr "Просмотреть MCP-серверы"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Компьютеры"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env."
|
msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Настраивается путем развертывания"
|
msgstr "Настраивается путем развертывания"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Подключено и используется {0}."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения."
|
msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Отключение…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr "Закрыть"
|
msgstr "Закрыть"
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Готово"
|
msgstr "Готово"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "Скачать {0}"
|
msgstr "Скачать {0}"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "Скачать {0}"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "Скачать {name}"
|
msgstr "Скачать {name}"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Развернуть"
|
msgstr "Развернуть"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "Развернуть {0}"
|
msgstr "Развернуть {0}"
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "Не удалось отправить сообщение"
|
msgstr "Не удалось отправить сообщение"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Привет, вот как я буду звучать, когда буду читать ответы вслух."
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr "Скрыть ботов"
|
msgstr "Скрыть ботов"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "Скрыть пароль"
|
msgstr "Скрыть пароль"
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "Импортировать OpenAPI JSON"
|
msgstr "Импортировать OpenAPI JSON"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "изолированный"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
|
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "Перейти к последнему"
|
msgstr "Перейти к последнему"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "Перейти к последнему"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Перейти к ответному сообщению"
|
msgstr "Перейти к ответному сообщению"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr "Новое пространство"
|
msgstr "Новое пространство"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "Поставщики не найдены."
|
msgstr "Поставщики не найдены."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr "Удаленные MCP-серверы не найдены"
|
msgstr "Удаленные MCP-серверы не найдены"
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "Нет результатов"
|
msgstr "Нет результатов"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "Запусков пока нет"
|
msgstr "Запусков пока нет"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Уведомления"
|
msgstr "Уведомления"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Сейчас"
|
msgstr "Сейчас"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Открыть"
|
msgstr "Открыть"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "Открыть в полном окне"
|
msgstr "Открыть в полном окне"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr "Приватное пространство со своими ботам
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Недавние"
|
msgstr "Недавние"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "Удалить {0}"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr "Удалить событие Git"
|
msgstr "Удалить событие Git"
|
||||||
@ -4653,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr "Восстановление вашего рабочего пространства"
|
msgstr "Восстановление вашего рабочего пространства"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Задача"
|
msgstr "Задача"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Задачи"
|
msgstr "Задачи"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "Запустить в"
|
msgstr "Запустить в"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "История запуска"
|
msgstr "История запуска"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Выполняется…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера."
|
msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера."
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Отправить ответ"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Отправить"
|
msgstr "Отправить"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Отправить сейчас"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Отправить ссылку для сброса"
|
msgstr "Отправить ссылку для сброса"
|
||||||
@ -5021,6 +5097,10 @@ msgstr "Показать ботов"
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Показать ещё"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "Показать пароль"
|
msgstr "Показать пароль"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Показать настройки"
|
msgstr "Показать настройки"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Остановить обучение"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Обучение в процессе. Прекратите обучен
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Команда"
|
msgstr "Команда"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "Вход по этой подписке пока недоступен в
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "Актуально"
|
msgstr "Актуально"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Вчера"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "Вы"
|
msgstr "Вы"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "Ваше имя"
|
msgstr "Ваше имя"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "{remaining} kaldı · bot yalnızca izliyor"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} çalışıyor"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "Executor ekle"
|
msgstr "Executor ekle"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "Her zaman izinli"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "Yanıtla"
|
msgstr "Yanıtla"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "Canlı masaüstü başlatılıyor…"
|
msgstr "Canlı masaüstü başlatılıyor…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "{0} adlı botun bilgisayarı başlatılıyor"
|
msgstr "{0} adlı botun bilgisayarı başlatılıyor"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Bot ekranı"
|
msgstr "Bot ekranı"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Bot ekranı önizlemesi"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1031,10 +1060,6 @@ msgstr ""
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "Botlar varsayılan olarak sormadan hareket eder. Yalnızca belirli bir işlem türünü önce incelemek istediğinde istisna ekle."
|
msgstr "Botlar varsayılan olarak sormadan hareket eder. Yalnızca belirli bir işlem türünü önce incelemek istediğinde istisna ekle."
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Botlar çalışıyor"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "Bilgisayarlar"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun."
|
msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun."
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "Dağıtım tarafından yapılandırıldı"
|
msgstr "Dağıtım tarafından yapılandırıldı"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "Bağlandı, {0} kullanılıyor."
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir."
|
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir."
|
||||||
@ -2322,6 +2354,7 @@ msgstr "Bağlantı kesiliyor…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Bitti"
|
msgstr "Bitti"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "{0} indir"
|
msgstr "{0} indir"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "{0} indir"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "{name} indir"
|
msgstr "{name} indir"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "Genişlet"
|
msgstr "Genişlet"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "Mesaj gönderilemedi"
|
msgstr "Mesaj gönderilemedi"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "Merhaba, yanıtları sesli okuduğumda böyle duyulacağım."
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "OpenAPI JSON içe aktar"
|
msgstr "OpenAPI JSON içe aktar"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "Yalıtılmış"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Sohbeti, dosyaları ve rutinleri kalıcı olarak silinecek. Oluşturduğu botlar listende kalır."
|
msgstr "Sohbeti, dosyaları ve rutinleri kalıcı olarak silinecek. Oluşturduğu botlar listende kalır."
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "En son mesaja git"
|
msgstr "En son mesaja git"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "En son mesaja git"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "Yanıtlanan mesaja git"
|
msgstr "Yanıtlanan mesaja git"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "Sağlayıcı bulunamadı."
|
msgstr "Sağlayıcı bulunamadı."
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "Sonuç yok"
|
msgstr "Sonuç yok"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "Bildirimler"
|
msgstr "Bildirimler"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "Şimdi"
|
msgstr "Şimdi"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Aç"
|
msgstr "Aç"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "Tam pencerede aç"
|
msgstr "Tam pencerede aç"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "Akıl yürütme düzeyi"
|
msgstr "Akıl yürütme düzeyi"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "Son"
|
msgstr "Son"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "{0} kaldır"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr ""
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "Rutin"
|
msgstr "Rutin"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "Rutinler"
|
msgstr "Rutinler"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "Çalışıyor…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "Yanıtı gönder"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "Gönder"
|
msgstr "Gönder"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "Şimdi gönder"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "Sıfırlama bağlantısı gönder"
|
msgstr "Sıfırlama bağlantısı gönder"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "Daha fazla göster"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "Ayarları göster"
|
msgstr "Ayarları göster"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "Öğretmeyi durdur"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "Öğretme sürüyor. Yeni mesaj göndermeden önce öğretmeyi durdur."
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "Takım"
|
msgstr "Takım"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "Bu abonelik girişi Rakazo'da henüz kullanılamıyor. Bir dağıtım ki
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "Dün"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "Sen"
|
msgstr "Sen"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "Adın"
|
msgstr "Adın"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -161,11 +161,6 @@ msgstr ""
|
|||||||
msgid "{0}'s browser appears here when the computer is running."
|
msgid "{0}'s browser appears here when the computer is running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "{0}'s screen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -182,10 +177,38 @@ msgstr ""
|
|||||||
msgid "{botName} clears the path…"
|
msgid "{botName} clears the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is checking a problem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "{botName} is on it…"
|
msgid "{botName} is on it…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is replying"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is researching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is thinking"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is waiting for your answer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "{botName} is working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
#: src/pages/CallView.tsx
|
#: src/pages/CallView.tsx
|
||||||
msgid "{botName} is working…"
|
msgid "{botName} is working…"
|
||||||
@ -364,6 +387,7 @@ msgstr "剩余 {remaining} · Bot 只看不操作"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -386,10 +410,6 @@ msgstr "{title},{label}"
|
|||||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "{workingBotName} is working"
|
|
||||||
msgstr "{workingBotName} 正在工作"
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
@ -544,11 +564,16 @@ msgstr ""
|
|||||||
msgid "Add as projects"
|
msgid "Add as projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NotificationSettingsPanel.tsx
|
||||||
|
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
msgid "Add Executor"
|
msgid "Add Executor"
|
||||||
msgstr "添加 Executor"
|
msgstr "添加 Executor"
|
||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Add goal"
|
msgid "Add goal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -737,6 +762,14 @@ msgstr "始终允许"
|
|||||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Analyze the project and give me a brief overview."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Analyzing project…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr "回答"
|
msgstr "回答"
|
||||||
@ -996,7 +1029,7 @@ msgstr ""
|
|||||||
msgid "Booting live desktop…"
|
msgid "Booting live desktop…"
|
||||||
msgstr "正在启动实时桌面…"
|
msgstr "正在启动实时桌面…"
|
||||||
|
|
||||||
#. placeholder {0}: active?.name
|
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Booting up {0}’s computer"
|
msgid "Booting up {0}’s computer"
|
||||||
msgstr "正在启动 {0} 的电脑"
|
msgstr "正在启动 {0} 的电脑"
|
||||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
|||||||
msgid "Bot screen"
|
msgid "Bot screen"
|
||||||
msgstr "Bot 屏幕"
|
msgstr "Bot 屏幕"
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Bot screen preview"
|
|
||||||
msgstr "Bot 屏幕预览"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Bot to link"
|
msgid "Bot to link"
|
||||||
msgstr "要关联的 Bot"
|
msgstr "要关联的 Bot"
|
||||||
@ -1031,10 +1060,6 @@ msgstr "要关联的 Bot"
|
|||||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||||
msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。"
|
msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。"
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
|
||||||
msgid "Bots are working"
|
|
||||||
msgstr "Bot 正在工作"
|
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Bow"
|
msgid "Bow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1064,7 +1089,6 @@ msgstr ""
|
|||||||
msgid "Browse MCP servers"
|
msgid "Browse MCP servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Browser"
|
msgid "Browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1493,6 +1517,10 @@ msgstr "电脑"
|
|||||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||||
msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。"
|
msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。"
|
||||||
|
|
||||||
|
#: src/pages/PluginsOverlay.tsx
|
||||||
|
msgid "Configure a plugin catalog on the server to connect apps."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Configured by deployment"
|
msgid "Configured by deployment"
|
||||||
msgstr "由部署配置"
|
msgstr "由部署配置"
|
||||||
@ -1584,6 +1612,10 @@ msgstr "已连接并使用 {0}。"
|
|||||||
msgid "Connected app"
|
msgid "Connected app"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Connected tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Connected. Its tools are available from your next message."
|
msgid "Connected. Its tools are available from your next message."
|
||||||
msgstr "已连接,下一条消息起即可使用其工具。"
|
msgstr "已连接,下一条消息起即可使用其工具。"
|
||||||
@ -2322,6 +2354,7 @@ msgstr "正在断开…"
|
|||||||
#: src/components/ComputerUpdateProgress.tsx
|
#: src/components/ComputerUpdateProgress.tsx
|
||||||
#: src/components/teach/TeachComputerOverlay.tsx
|
#: src/components/teach/TeachComputerOverlay.tsx
|
||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Dismiss"
|
msgid "Dismiss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2381,7 +2414,9 @@ msgstr ""
|
|||||||
#: src/components/ThreadCard.tsx
|
#: src/components/ThreadCard.tsx
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/project-overview.tsx
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "完成"
|
msgstr "完成"
|
||||||
|
|
||||||
@ -2394,8 +2429,11 @@ msgstr ""
|
|||||||
msgid "Dot"
|
msgid "Dot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: activeItem.name
|
||||||
|
#. placeholder {0}: item.name
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Download {0}"
|
msgid "Download {0}"
|
||||||
msgstr "下载 {0}"
|
msgstr "下载 {0}"
|
||||||
|
|
||||||
@ -2403,7 +2441,6 @@ msgstr "下载 {0}"
|
|||||||
#: src/components/ArtifactPane.tsx
|
#: src/components/ArtifactPane.tsx
|
||||||
#: src/components/media/MediaCard.tsx
|
#: src/components/media/MediaCard.tsx
|
||||||
#: src/components/media/MediaPlayer.tsx
|
#: src/components/media/MediaPlayer.tsx
|
||||||
#: src/pages/shell/message-cards.tsx
|
|
||||||
msgid "Download {name}"
|
msgid "Download {name}"
|
||||||
msgstr "下载 {name}"
|
msgstr "下载 {name}"
|
||||||
|
|
||||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
|||||||
msgstr "展开"
|
msgstr "展开"
|
||||||
|
|
||||||
#. placeholder {0}: group.title
|
#. placeholder {0}: group.title
|
||||||
|
#. placeholder {0}: item.name
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/sidebar-group-header.tsx
|
#: src/pages/shell/sidebar-group-header.tsx
|
||||||
msgid "Expand {0}"
|
msgid "Expand {0}"
|
||||||
msgstr "展开 {0}"
|
msgstr "展开 {0}"
|
||||||
@ -2660,6 +2699,10 @@ msgstr ""
|
|||||||
msgid "Failed to open file"
|
msgid "Failed to open file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "Failed to remove message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Failed to send message"
|
msgid "Failed to send message"
|
||||||
msgstr "发送消息失败"
|
msgstr "发送消息失败"
|
||||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
msgid "Hi, this is how I'll sound when we talk."
|
||||||
msgstr "你好,这是我朗读回复时的声音。"
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/sidebar-header.tsx
|
||||||
msgid "Hide bots"
|
msgid "Hide bots"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Hide details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Hide password"
|
msgid "Hide password"
|
||||||
msgstr "隐藏密码"
|
msgstr "隐藏密码"
|
||||||
@ -3080,6 +3128,7 @@ msgstr ""
|
|||||||
msgid "Import OpenAPI JSON"
|
msgid "Import OpenAPI JSON"
|
||||||
msgstr "导入 OpenAPI JSON"
|
msgstr "导入 OpenAPI JSON"
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
#: src/pages/shell/run-steps.tsx
|
#: src/pages/shell/run-steps.tsx
|
||||||
msgid "In Bearbeitung"
|
msgid "In Bearbeitung"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3166,6 +3215,10 @@ msgstr "隔离"
|
|||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
|
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/transcript.tsx
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "Jump to latest"
|
msgid "Jump to latest"
|
||||||
msgstr "跳到最新"
|
msgstr "跳到最新"
|
||||||
@ -3174,6 +3227,10 @@ msgstr "跳到最新"
|
|||||||
msgid "Jump to replied message"
|
msgid "Jump to replied message"
|
||||||
msgstr "跳到被回复的消息"
|
msgstr "跳到被回复的消息"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Just ask in the chat whenever something should happen regularly."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "just now"
|
msgid "just now"
|
||||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
|||||||
msgstr "新建工作区"
|
msgstr "新建工作区"
|
||||||
|
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3885,6 +3943,10 @@ msgstr ""
|
|||||||
msgid "No providers found."
|
msgid "No providers found."
|
||||||
msgstr "未找到提供方。"
|
msgstr "未找到提供方。"
|
||||||
|
|
||||||
|
#: src/pages/ActivityList.tsx
|
||||||
|
msgid "No recent activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
msgid "No remote MCP servers found"
|
msgid "No remote MCP servers found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
|||||||
msgstr "无结果"
|
msgstr "无结果"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "No runs yet"
|
msgid "No runs yet"
|
||||||
msgstr "还没有运行记录"
|
msgstr "还没有运行记录"
|
||||||
|
|
||||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
|||||||
msgstr "通知"
|
msgstr "通知"
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Now"
|
msgid "Now"
|
||||||
msgstr "现在"
|
msgstr "现在"
|
||||||
|
|
||||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "打开"
|
msgstr "打开"
|
||||||
|
|
||||||
@ -4056,7 +4119,7 @@ msgstr ""
|
|||||||
msgid "Open in browser"
|
msgid "Open in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/components/browser/OoComputerBrowser.tsx
|
||||||
msgid "Open in full window"
|
msgid "Open in full window"
|
||||||
msgstr "在完整窗口中打开"
|
msgstr "在完整窗口中打开"
|
||||||
|
|
||||||
@ -4288,10 +4351,6 @@ msgstr ""
|
|||||||
msgid "Project"
|
msgid "Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Project · sessions & folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
#: src/pages/shell/project-look-dialog.tsx
|
#: src/pages/shell/project-look-dialog.tsx
|
||||||
msgid "Project folder"
|
msgid "Project folder"
|
||||||
@ -4399,6 +4458,10 @@ msgstr ""
|
|||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Reasoning effort"
|
msgid "Reasoning effort"
|
||||||
msgstr "推理强度"
|
msgstr "推理强度"
|
||||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ActivityList.tsx
|
#: src/pages/ActivityList.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Recent"
|
msgid "Recent"
|
||||||
msgstr "最近"
|
msgstr "最近"
|
||||||
|
|
||||||
@ -4528,6 +4592,10 @@ msgstr "移除 {0}"
|
|||||||
msgid "Remove folder"
|
msgid "Remove folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Remove from queue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Remove Git event"
|
msgid "Remove Git event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4653,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。
|
|||||||
msgid "Restoring your workspace"
|
msgid "Restoring your workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Resume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/FolderPickerDialog.tsx
|
#: src/components/FolderPickerDialog.tsx
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4700,11 +4773,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Routine"
|
msgid "Routine"
|
||||||
msgstr "例行任务"
|
msgstr "例行任务"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Routines"
|
msgid "Routines"
|
||||||
msgstr "例行任务"
|
msgstr "例行任务"
|
||||||
|
|
||||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
|||||||
msgstr "运行时间"
|
msgstr "运行时间"
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Run history"
|
msgid "Run history"
|
||||||
msgstr "运行记录"
|
msgstr "运行记录"
|
||||||
|
|
||||||
@ -4756,10 +4832,6 @@ msgstr "正在运行…"
|
|||||||
msgid "Runs when this bot receives a verified message from this provider."
|
msgid "Runs when this bot receives a verified message from this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
|
||||||
msgid "Sandbox"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
#: src/components/MediaSettings.tsx
|
#: src/components/MediaSettings.tsx
|
||||||
#: src/components/teach/SkillDraftCard.tsx
|
#: src/components/teach/SkillDraftCard.tsx
|
||||||
@ -4909,6 +4981,10 @@ msgstr "发送回答"
|
|||||||
msgid "Send it"
|
msgid "Send it"
|
||||||
msgstr "发送"
|
msgstr "发送"
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Send now"
|
||||||
|
msgstr "立即发送"
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Send reset link"
|
msgid "Send reset link"
|
||||||
msgstr "发送重置链接"
|
msgstr "发送重置链接"
|
||||||
@ -5021,6 +5097,10 @@ msgstr ""
|
|||||||
msgid "Show computer"
|
msgid "Show computer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
|
msgid "Show details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5037,6 +5117,7 @@ msgstr "显示更多"
|
|||||||
msgid "Show password"
|
msgid "Show password"
|
||||||
msgstr "显示密码"
|
msgstr "显示密码"
|
||||||
|
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
#: src/pages/shell/side-panel-header.tsx
|
#: src/pages/shell/side-panel-header.tsx
|
||||||
msgid "Show settings"
|
msgid "Show settings"
|
||||||
msgstr "显示设置"
|
msgstr "显示设置"
|
||||||
@ -5172,6 +5253,7 @@ msgstr ""
|
|||||||
msgid "Speaking with intent…"
|
msgid "Speaking with intent…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
msgid "Specialist crew"
|
msgid "Specialist crew"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -5286,6 +5368,10 @@ msgstr "停止示范"
|
|||||||
msgid "Stop timer"
|
msgid "Stop timer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Stopped"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. js-lingui-explicit-id
|
#. js-lingui-explicit-id
|
||||||
#: src/lib/browser-notifications.ts
|
#: src/lib/browser-notifications.ts
|
||||||
msgid "Stopped."
|
msgid "Stopped."
|
||||||
@ -5398,6 +5484,7 @@ msgstr "正在示范,发送新消息前请先停止示范。"
|
|||||||
|
|
||||||
#: src/pages/ProjectSessionTimeline.tsx
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Team"
|
msgid "Team"
|
||||||
msgstr "团队"
|
msgstr "团队"
|
||||||
|
|
||||||
@ -5512,10 +5599,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选
|
|||||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Threads by topic · folders"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelTierSettings.tsx
|
#: src/pages/ModelTierSettings.tsx
|
||||||
#: src/pages/shell/bot-panel.tsx
|
#: src/pages/shell/bot-panel.tsx
|
||||||
msgid "Tiers"
|
msgid "Tiers"
|
||||||
@ -5701,6 +5784,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
|
#: src/pages/AccountSettingsOverlay.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
msgstr "已是最新"
|
msgstr "已是最新"
|
||||||
|
|
||||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/stage/OoStage.tsx
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/shell/project-overview.tsx
|
||||||
msgid "Working"
|
msgid "Working"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6037,6 +6122,7 @@ msgstr "昨天"
|
|||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/transcript.tsx
|
||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr "你"
|
msgstr "你"
|
||||||
|
|
||||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
|||||||
msgstr "你的名字"
|
msgstr "你的名字"
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "Your Oo reads replies aloud when this is on."
|
msgid "Your Oo talks with you naturally when this is on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Your Oo will communicate in this language. You can change it later."
|
msgid "Your Oo will communicate in this language. You can change it later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||||
|
msgid "Your profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Welcome.tsx
|
#: src/pages/Welcome.tsx
|
||||||
msgid "Your specialists. One calm place to talk."
|
msgid "Your specialists. One calm place to talk."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/message-cards.tsx
|
||||||
|
msgid "Zoom out"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<DesktopUpdateSection />
|
<DesktopUpdateSection />
|
||||||
<SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} />
|
<SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} />
|
||||||
|
<section className="hidden rounded-xl border border-border px-4 py-4 only:block">
|
||||||
|
<p className="text-ui-body-sm text-muted-foreground">
|
||||||
|
<Trans>Up to date</Trans>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,13 @@ export function ActivityList({ onOpenRun }: ActivityListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeRuns.length === 0 && recentRuns.length === 0) return null;
|
if (activeRuns.length === 0 && recentRuns.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2 border-b border-border px-2.5 py-2 text-ui-body-sm text-muted-foreground/80">
|
||||||
|
<Trans>No recent activity</Trans>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 border-b border-border pb-2">
|
<div className="mb-2 border-b border-border pb-2">
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { useLingui } from "@lingui/react/macro";
|
import { useLingui } from "@lingui/react/macro";
|
||||||
import type { ThreadMessage, ThreadSnapshot } from "@rakazo/contracts";
|
import type { ThreadMessage, ThreadSnapshot } from "@rakazo/contracts";
|
||||||
import { isSecretAskBlock, speechFromBlocks, spokenDecision } from "@rakazo/core";
|
import {
|
||||||
|
isSecretAskBlock,
|
||||||
|
speechFromBlocks,
|
||||||
|
spokenDecision,
|
||||||
|
spokenSummaryFromBlocks,
|
||||||
|
} from "@rakazo/core";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage";
|
import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage";
|
||||||
import { VoiceStage } from "../components/voice/VoiceStage";
|
import { VoiceStage } from "../components/voice/VoiceStage";
|
||||||
@ -417,7 +422,7 @@ export function CallView({
|
|||||||
(block) => block.kind === "ask" && block.status !== "answered",
|
(block) => block.kind === "ask" && block.status !== "answered",
|
||||||
);
|
);
|
||||||
const secretAsk = ask && isSecretAskBlock(ask);
|
const secretAsk = ask && isSecretAskBlock(ask);
|
||||||
const rawText = speechFromBlocks(lastBot.blocks);
|
const rawText = spokenSummaryFromBlocks(lastBot.blocks);
|
||||||
const text = rawText
|
const text = rawText
|
||||||
? secretAsk
|
? secretAsk
|
||||||
? `${rawText}. ${secretPromptRef.current}`
|
? `${rawText}. ${secretPromptRef.current}`
|
||||||
@ -477,7 +482,7 @@ export function CallView({
|
|||||||
hasUnspokenBotMessage: Boolean(
|
hasUnspokenBotMessage: Boolean(
|
||||||
lastBot &&
|
lastBot &&
|
||||||
lastBot.id !== spokenMessage.current &&
|
lastBot.id !== spokenMessage.current &&
|
||||||
speechFromBlocks(lastBot.blocks).trim(),
|
spokenSummaryFromBlocks(lastBot.blocks).trim(),
|
||||||
),
|
),
|
||||||
runActive: isActiveRunStatus(snapshot?.run?.status),
|
runActive: isActiveRunStatus(snapshot?.run?.status),
|
||||||
userTurnPending: userTurnPendingRef.current,
|
userTurnPending: userTurnPendingRef.current,
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import type {
|
|||||||
Group,
|
Group,
|
||||||
HabitToday,
|
HabitToday,
|
||||||
RunActivityRow,
|
RunActivityRow,
|
||||||
|
Space,
|
||||||
|
SpaceBot,
|
||||||
|
SpaceGroup,
|
||||||
} from "@rakazo/contracts";
|
} from "@rakazo/contracts";
|
||||||
import { runChatTitle } from "@rakazo/core";
|
import { runChatTitle } from "@rakazo/core";
|
||||||
import { botColors } from "@rakazo/ui-tokens";
|
import { botColors } from "@rakazo/ui-tokens";
|
||||||
@ -30,6 +33,7 @@ import { rpc } from "../lib/rpc";
|
|||||||
type HeuteBriefProps = {
|
type HeuteBriefProps = {
|
||||||
bots: Bot[];
|
bots: Bot[];
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
|
spaces?: Space[];
|
||||||
onOpenBot: (botId: string, spaceId: string) => void;
|
onOpenBot: (botId: string, spaceId: string) => void;
|
||||||
onOpenGroup: (groupId: string, spaceId: string) => void;
|
onOpenGroup: (groupId: string, spaceId: string) => void;
|
||||||
onOpenRun: (run: RunActivityRow) => void;
|
onOpenRun: (run: RunActivityRow) => void;
|
||||||
@ -41,6 +45,7 @@ const DATE_LABEL: Intl.DateTimeFormatOptions = { weekday: "long", day: "numeric"
|
|||||||
export function HeuteBrief({
|
export function HeuteBrief({
|
||||||
bots,
|
bots,
|
||||||
groups,
|
groups,
|
||||||
|
spaces = [],
|
||||||
onOpenBot,
|
onOpenBot,
|
||||||
onOpenGroup,
|
onOpenGroup,
|
||||||
onOpenRun,
|
onOpenRun,
|
||||||
@ -135,13 +140,21 @@ export function HeuteBrief({
|
|||||||
buildHeuteBrief({
|
buildHeuteBrief({
|
||||||
bots,
|
bots,
|
||||||
groups,
|
groups,
|
||||||
|
spaces,
|
||||||
activeRuns,
|
activeRuns,
|
||||||
recentRuns,
|
recentRuns,
|
||||||
}),
|
}),
|
||||||
[activeRuns, bots, groups, recentRuns],
|
[activeRuns, bots, groups, recentRuns, spaces],
|
||||||
);
|
);
|
||||||
|
|
||||||
const botColorById = useMemo(() => new Map(bots.map((bot) => [bot.id, bot.color])), [bots]);
|
const botColorById = useMemo(
|
||||||
|
() =>
|
||||||
|
new Map([
|
||||||
|
...spaces.flatMap((space) => space.bots).map((bot) => [bot.id, bot.color] as const),
|
||||||
|
...bots.map((bot) => [bot.id, bot.color] as const),
|
||||||
|
]),
|
||||||
|
[bots, spaces],
|
||||||
|
);
|
||||||
|
|
||||||
const locale = i18n.locale || "de";
|
const locale = i18n.locale || "de";
|
||||||
const dateLabel = coach
|
const dateLabel = coach
|
||||||
@ -391,13 +404,13 @@ function NeedsYouRow({
|
|||||||
>
|
>
|
||||||
{chatKind === "bot" ? (
|
{chatKind === "bot" ? (
|
||||||
<BotAvatar
|
<BotAvatar
|
||||||
color={(chat as Bot).color}
|
color={(chat as SpaceBot).color}
|
||||||
identity={chat.id}
|
identity={chat.id}
|
||||||
size={40}
|
size={40}
|
||||||
status={(chat as Bot).status}
|
status={(chat as SpaceBot).status}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<GroupAvatar members={(chat as Group).members} size={40} />
|
<GroupAvatar members={(chat as SpaceGroup).members} size={40} />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-ui-body font-semibold text-foreground">{title}</div>
|
<div className="truncate text-ui-body font-semibold text-foreground">{title}</div>
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
abortableDelay,
|
abortableDelay,
|
||||||
buildFeaturedConnectorTiles,
|
buildFeaturedConnectorTiles,
|
||||||
CONNECTION_CATALOG_PAGE_SIZE,
|
CONNECTION_CATALOG_PAGE_SIZE,
|
||||||
EMPTY_PLUGIN_CATALOG_MESSAGE,
|
|
||||||
filterConnectionCatalogItems,
|
filterConnectionCatalogItems,
|
||||||
humanizeToolName,
|
humanizeToolName,
|
||||||
isGithubInstall,
|
isGithubInstall,
|
||||||
@ -730,7 +729,7 @@ export function PluginsOverlay({
|
|||||||
<div className="mb-6" data-testid="featured-connectors">
|
<div className="mb-6" data-testid="featured-connectors">
|
||||||
{!loading && catalog.length === 0 ? (
|
{!loading && catalog.length === 0 ? (
|
||||||
<p className="text-ui-body leading-6 text-muted-foreground/80">
|
<p className="text-ui-body leading-6 text-muted-foreground/80">
|
||||||
{EMPTY_PLUGIN_CATALOG_MESSAGE}
|
<Trans>Configure a plugin catalog on the server to connect apps.</Trans>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
|||||||
@ -176,7 +176,7 @@ export function SettingsOverlay({
|
|||||||
<nav
|
<nav
|
||||||
data-testid="settings-nav"
|
data-testid="settings-nav"
|
||||||
aria-label={t`Settings`}
|
aria-label={t`Settings`}
|
||||||
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[200px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
|
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[220px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
|
||||||
>
|
>
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import type { VoiceCredential, VoiceInfo, VoiceStatus } from "@rakazo/contracts";
|
import type { VoiceCredential, VoiceInfo, VoiceStatus } from "@rakazo/contracts";
|
||||||
import { voiceIdForLocale, voicesForLocale } from "@rakazo/core";
|
import { GERMAN_VOICE_PRESETS, voiceIdForLocale, voicesForLocale } from "@rakazo/core";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
cn,
|
cn,
|
||||||
@ -46,7 +46,7 @@ export function VoiceSettingsOverlay({
|
|||||||
const [voiceId, setVoiceId] = useState("");
|
const [voiceId, setVoiceId] = useState("");
|
||||||
const [autoSpeak, setAutoSpeak] = useState(false);
|
const [autoSpeak, setAutoSpeak] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pending, setPending] = useState<"connect" | "voice" | "speak" | null>(null);
|
const [pending, setPending] = useState<"voice" | "speak" | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [notice, setNotice] = useState<string | null>(null);
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
const [soundEnabled, setSoundEnabled] = useState(() => readOoSoundEnabled());
|
const [soundEnabled, setSoundEnabled] = useState(() => readOoSoundEnabled());
|
||||||
@ -74,7 +74,7 @@ export function VoiceSettingsOverlay({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function markPending(next: "connect" | "voice" | "speak" | null) {
|
function markPending(next: "voice" | "speak" | null) {
|
||||||
setPending(next);
|
setPending(next);
|
||||||
onBusyChange?.(next !== null);
|
onBusyChange?.(next !== null);
|
||||||
}
|
}
|
||||||
@ -113,17 +113,22 @@ export function VoiceSettingsOverlay({
|
|||||||
}, [activeBotId]);
|
}, [activeBotId]);
|
||||||
|
|
||||||
const voiceOptions = useMemo(() => {
|
const voiceOptions = useMemo(() => {
|
||||||
const listed = voicesForLocale(voices, uiLocale);
|
const isGermanLocale = uiLocale.toLowerCase().startsWith("de");
|
||||||
|
const hasGermanVoices = voices.some((v) => /^d[fm]_/i.test(v.id));
|
||||||
|
const combined =
|
||||||
|
isGermanLocale && !hasGermanVoices ? [...GERMAN_VOICE_PRESETS, ...voices] : voices;
|
||||||
|
const listed = voicesForLocale(combined, uiLocale);
|
||||||
if (listed.length) return listed;
|
if (listed.length) return listed;
|
||||||
|
if (isGermanLocale) return GERMAN_VOICE_PRESETS;
|
||||||
return voiceId ? [{ id: voiceId, label: voiceId }] : [];
|
return voiceId ? [{ id: voiceId, label: voiceId }] : [];
|
||||||
}, [voices, voiceId, uiLocale]);
|
}, [voices, voiceId, uiLocale]);
|
||||||
|
|
||||||
const speakingReady = Boolean(localConnected && voiceId && status?.ready);
|
const speakingReady = Boolean(
|
||||||
|
(localConnected && voiceId && status?.ready) || (autoSpeak && voiceOptions.length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
async function ensureLocalVoice(): Promise<boolean> {
|
async function ensureLocalVoice(): Promise<boolean> {
|
||||||
if (localConnected) return true;
|
if (localConnected) return true;
|
||||||
markPending("connect");
|
|
||||||
setError(null);
|
|
||||||
try {
|
try {
|
||||||
await rpc.voice.connect({
|
await rpc.voice.connect({
|
||||||
provider: LOCAL_PROVIDER_ID,
|
provider: LOCAL_PROVIDER_ID,
|
||||||
@ -133,16 +138,22 @@ export function VoiceSettingsOverlay({
|
|||||||
await refresh();
|
await refresh();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
setError(err instanceof Error ? err.message : t`Could not enable speaking`);
|
setError(err instanceof Error ? err.message : t`Could not enable speaking`);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
|
||||||
markPending(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseVoice(nextVoiceId: string) {
|
async function chooseVoice(nextVoiceId: string) {
|
||||||
setVoiceId(nextVoiceId);
|
setVoiceId(nextVoiceId);
|
||||||
if (!localConnected) return;
|
if (!localConnected) {
|
||||||
|
if (activeBotId) {
|
||||||
|
await rpc.bots.update({ botId: activeBotId, voiceId: nextVoiceId }).catch(() => undefined);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
markPending("voice");
|
markPending("voice");
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@ -161,6 +172,7 @@ export function VoiceSettingsOverlay({
|
|||||||
setError(null);
|
setError(null);
|
||||||
setNotice(null);
|
setNotice(null);
|
||||||
try {
|
try {
|
||||||
|
let chosenVoice = voiceId;
|
||||||
if (next) {
|
if (next) {
|
||||||
const connected = await ensureLocalVoice();
|
const connected = await ensureLocalVoice();
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
@ -168,16 +180,29 @@ export function VoiceSettingsOverlay({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const listed = await rpc.voice.voices({ provider: LOCAL_PROVIDER_ID }).catch(() => []);
|
const listed = await rpc.voice.voices({ provider: LOCAL_PROVIDER_ID }).catch(() => []);
|
||||||
const nextVoice = voiceIdForLocale(listed, uiLocale, voiceId);
|
const isGermanLocale = uiLocale.toLowerCase().startsWith("de");
|
||||||
|
const pool =
|
||||||
|
isGermanLocale && !listed.some((v) => /^d[fm]_/i.test(v.id))
|
||||||
|
? [...GERMAN_VOICE_PRESETS, ...listed]
|
||||||
|
: listed;
|
||||||
|
const nextVoice = voiceIdForLocale(pool, uiLocale, voiceId);
|
||||||
if (nextVoice) {
|
if (nextVoice) {
|
||||||
await rpc.voice.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID });
|
chosenVoice = nextVoice;
|
||||||
setVoiceId(nextVoice);
|
setVoiceId(nextVoice);
|
||||||
|
await rpc.voice
|
||||||
|
.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID })
|
||||||
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (activeBotId) {
|
if (activeBotId) {
|
||||||
await rpc.bots.update({ botId: activeBotId, autoSpeak: next });
|
await rpc.bots.update({
|
||||||
|
botId: activeBotId,
|
||||||
|
autoSpeak: next,
|
||||||
|
...(chosenVoice ? { voiceId: chosenVoice } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await refresh();
|
await refresh();
|
||||||
|
if (!voiceId && chosenVoice) setVoiceId(chosenVoice);
|
||||||
setNotice(next ? t`Speaking is on.` : t`Speaking is off.`);
|
setNotice(next ? t`Speaking is on.` : t`Speaking is off.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAutoSpeak(!next);
|
setAutoSpeak(!next);
|
||||||
@ -193,7 +218,11 @@ export function VoiceSettingsOverlay({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
void speaker.speak(t`Hi, this is how I'll sound when I read replies out loud.`, {
|
const isGermanVoice = /^d[fm]_/i.test(voice.id) || uiLocale.toLowerCase().startsWith("de");
|
||||||
|
const sampleText = isGermanVoice
|
||||||
|
? `Hallo, ich bin ${voice.label}. Lass uns direkt miteinander sprechen.`
|
||||||
|
: t`Hi, this is how I'll sound when we talk.`;
|
||||||
|
void speaker.speak(sampleText, {
|
||||||
voiceId: voice.id,
|
voiceId: voice.id,
|
||||||
messageId: `${PREVIEW_PREFIX}${voice.id}`,
|
messageId: `${PREVIEW_PREFIX}${voice.id}`,
|
||||||
});
|
});
|
||||||
@ -234,7 +263,7 @@ export function VoiceSettingsOverlay({
|
|||||||
{autoSpeak ? <Trans>Can speak</Trans> : <Trans>Does not speak</Trans>}
|
{autoSpeak ? <Trans>Can speak</Trans> : <Trans>Does not speak</Trans>}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-ui-body-sm text-muted-foreground">
|
<p className="mt-1 text-ui-body-sm text-muted-foreground">
|
||||||
<Trans>Your Oo reads replies aloud when this is on.</Trans>
|
<Trans>Your Oo talks with you naturally when this is on.</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
ModelTierSchema,
|
ModelTierSchema,
|
||||||
normalizeBotTags,
|
normalizeBotTags,
|
||||||
} from "@rakazo/contracts";
|
} from "@rakazo/contracts";
|
||||||
|
import { GERMAN_VOICE_PRESETS, germanVoiceForBot, voicesForLocale } from "@rakazo/core";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@ -37,6 +38,7 @@ import { connectedModelOptions } from "../../lib/connected-models";
|
|||||||
import { lazyWithRecovery } from "../../lib/lazy-recovery";
|
import { lazyWithRecovery } from "../../lib/lazy-recovery";
|
||||||
import { modelTierLabel } from "../../lib/model-tier-label";
|
import { modelTierLabel } from "../../lib/model-tier-label";
|
||||||
import { rpc } from "../../lib/rpc";
|
import { rpc } from "../../lib/rpc";
|
||||||
|
import { readUiLocale } from "../../lib/ui-locale";
|
||||||
import { BotCapabilitiesFields } from "./bot-capabilities";
|
import { BotCapabilitiesFields } from "./bot-capabilities";
|
||||||
|
|
||||||
const ScratchpadSection = lazyWithRecovery("ScratchpadSection", () =>
|
const ScratchpadSection = lazyWithRecovery("ScratchpadSection", () =>
|
||||||
@ -280,8 +282,8 @@ export function BotSettings({
|
|||||||
const [computerMode, setComputerMode] = useState(bot.computerMode);
|
const [computerMode, setComputerMode] = useState(bot.computerMode);
|
||||||
const [memoryScope, setMemoryScope] = useState(bot.memoryScope);
|
const [memoryScope, setMemoryScope] = useState(bot.memoryScope);
|
||||||
const [autoSpeak, setAutoSpeak] = useState(bot.autoSpeak);
|
const [autoSpeak, setAutoSpeak] = useState(bot.autoSpeak);
|
||||||
const [voiceId, setVoiceId] = useState(bot.voiceId ?? "");
|
const [voiceId, setVoiceId] = useState(bot.voiceId || germanVoiceForBot(bot.id, bot.voiceId));
|
||||||
const [voices, setVoices] = useState<VoiceInfo[]>([]);
|
const [voices, setVoices] = useState<VoiceInfo[]>(GERMAN_VOICE_PRESETS);
|
||||||
const [modelKey, setModelKey] = useState(
|
const [modelKey, setModelKey] = useState(
|
||||||
bot.modelTier
|
bot.modelTier
|
||||||
? tierOptionKey(bot.modelTier)
|
? tierOptionKey(bot.modelTier)
|
||||||
@ -314,8 +316,15 @@ export function BotSettings({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void rpc.voice
|
void rpc.voice
|
||||||
.voices({})
|
.voices({})
|
||||||
.then(setVoices)
|
.then((listed) => {
|
||||||
.catch(() => setVoices([]));
|
const uiLocale = readUiLocale();
|
||||||
|
const isGerman = uiLocale.toLowerCase().startsWith("de");
|
||||||
|
const hasGerman = listed.some((v) => /^d[fm]_/i.test(v.id));
|
||||||
|
const combined = isGerman && !hasGerman ? [...GERMAN_VOICE_PRESETS, ...listed] : listed;
|
||||||
|
const filtered = voicesForLocale(combined, uiLocale);
|
||||||
|
setVoices(filtered.length > 0 ? filtered : GERMAN_VOICE_PRESETS);
|
||||||
|
})
|
||||||
|
.catch(() => setVoices(GERMAN_VOICE_PRESETS));
|
||||||
void rpc.bots
|
void rpc.bots
|
||||||
.builtinTools({ botId: bot.id })
|
.builtinTools({ botId: bot.id })
|
||||||
.then(setToolNames)
|
.then(setToolNames)
|
||||||
@ -675,7 +684,7 @@ export function BotSettings({
|
|||||||
<NativeSelectOption value="">{t`Account default`}</NativeSelectOption>
|
<NativeSelectOption value="">{t`Account default`}</NativeSelectOption>
|
||||||
{voices.map((voice) => (
|
{voices.map((voice) => (
|
||||||
<NativeSelectOption key={voice.id} value={voice.id}>
|
<NativeSelectOption key={voice.id} value={voice.id}>
|
||||||
{voice.label}
|
{voice.description ? `${voice.label} (${voice.description})` : voice.label}
|
||||||
</NativeSelectOption>
|
</NativeSelectOption>
|
||||||
))}
|
))}
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
|
|||||||
249
apps/web/src/pages/shell/composer-task-pill.test.tsx
Normal file
249
apps/web/src/pages/shell/composer-task-pill.test.tsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { i18n } from "@lingui/core";
|
||||||
|
import { I18nProvider } from "@lingui/react";
|
||||||
|
import type { ThreadMessage } from "@rakazo/contracts";
|
||||||
|
import { act } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
function formatTemplate(parts: TemplateStringsArray, ...values: unknown[]) {
|
||||||
|
return parts.reduce(
|
||||||
|
(text, part, index) => text + part + (index < values.length ? values[index] : ""),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@lingui/core/macro", () => ({
|
||||||
|
t: formatTemplate,
|
||||||
|
msg: (parts: TemplateStringsArray, ...values: unknown[]) => ({
|
||||||
|
id: parts.join(""),
|
||||||
|
message: formatTemplate(parts, ...values),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@lingui/react/macro", () => {
|
||||||
|
return {
|
||||||
|
useLingui: () => ({ t: formatTemplate }),
|
||||||
|
Trans: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
Plural: ({ value, one, other }: { value: number; one: string; other: string }) =>
|
||||||
|
(value === 1 ? one : other).replace("#", String(value)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ComposerTaskPill, extractActivePlanState, PlanCard } from "./message-cards";
|
||||||
|
|
||||||
|
// @ts-expect-error React act environment flag
|
||||||
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
function buildSampleMessages(): ThreadMessage[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "u1",
|
||||||
|
threadId: "t1",
|
||||||
|
seq: 1,
|
||||||
|
role: "user",
|
||||||
|
runId: "r1",
|
||||||
|
createdAt: "2026-10-22T12:00:00Z",
|
||||||
|
blocks: [{ kind: "text", text: "Build brand concept" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b-plan",
|
||||||
|
threadId: "t1",
|
||||||
|
seq: 2,
|
||||||
|
role: "bot",
|
||||||
|
botId: "bot-1",
|
||||||
|
runId: "r1",
|
||||||
|
createdAt: "2026-10-22T12:00:05Z",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: "plan",
|
||||||
|
planId: "p1",
|
||||||
|
title: "Branding-Konzept finalisieren",
|
||||||
|
status: "active",
|
||||||
|
steps: [
|
||||||
|
{ id: "s1", label: "Slogans erstellen", status: "completed" },
|
||||||
|
{
|
||||||
|
id: "s2",
|
||||||
|
label: "Brand Guide HTML-Seite erstellen",
|
||||||
|
status: "in_progress",
|
||||||
|
note: "Komplette Zusammenfassung aller Brand-Elemente",
|
||||||
|
},
|
||||||
|
{ id: "s3", label: "Alles auf Server kopieren", status: "pending" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b-step-1",
|
||||||
|
threadId: "t1",
|
||||||
|
seq: 3,
|
||||||
|
role: "bot",
|
||||||
|
botId: "bot-1",
|
||||||
|
runId: "r1",
|
||||||
|
createdAt: "2026-10-22T12:00:10Z",
|
||||||
|
blocks: [{ kind: "text", text: "running: pnpm build" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b-step-2",
|
||||||
|
threadId: "t1",
|
||||||
|
seq: 4,
|
||||||
|
role: "bot",
|
||||||
|
botId: "bot-1",
|
||||||
|
runId: "r1",
|
||||||
|
createdAt: "2026-10-22T12:00:15Z",
|
||||||
|
blocks: [{ kind: "text", text: "writing brand-guide.html" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "progress:r1",
|
||||||
|
threadId: "t1",
|
||||||
|
seq: 5,
|
||||||
|
role: "bot",
|
||||||
|
botId: "bot-1",
|
||||||
|
runId: "r1",
|
||||||
|
createdAt: "2026-10-22T12:00:20Z",
|
||||||
|
blocks: [{ kind: "progress", text: "Working..." }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ComposerTaskPill & PlanCard integration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
i18n.load("en", {});
|
||||||
|
i18n.activate("en");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extractActivePlanState collects the active plan and folded narration steps from the current turn", () => {
|
||||||
|
const messages = buildSampleMessages();
|
||||||
|
const state = extractActivePlanState(messages, true, "2026-10-22T12:00:00Z");
|
||||||
|
expect(state).not.toBeNull();
|
||||||
|
expect(state?.block.planId).toBe("p1");
|
||||||
|
expect(state?.steps.map((s) => s.id)).toEqual(["b-step-1", "b-step-2"]);
|
||||||
|
expect(state?.running).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders compact active step + progress badge and expands to reveal full checklist & intermediate steps", async () => {
|
||||||
|
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
|
<ComposerTaskPill activePlan={state} />
|
||||||
|
</I18nProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pill = container.querySelector('[data-testid="composer-task-pill"]');
|
||||||
|
expect(pill).not.toBeNull();
|
||||||
|
expect(pill?.getAttribute("data-expanded")).toBe("false");
|
||||||
|
expect(container.querySelector('[data-testid="composer-task-pill-label"]')?.textContent).toBe(
|
||||||
|
"Brand Guide HTML-Seite erstellen",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
||||||
|
).toBe("1/3");
|
||||||
|
|
||||||
|
const toggle = container.querySelector(
|
||||||
|
'[data-testid="composer-task-pill-toggle"]',
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
await act(async () => {
|
||||||
|
toggle.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pill?.getAttribute("data-expanded")).toBe("true");
|
||||||
|
const intermediate = container.querySelector(
|
||||||
|
'[data-testid="composer-plan-intermediate-steps"]',
|
||||||
|
);
|
||||||
|
expect(intermediate).not.toBeNull();
|
||||||
|
|
||||||
|
const stepToggle = intermediate?.querySelector("button") as HTMLButtonElement;
|
||||||
|
await act(async () => {
|
||||||
|
stepToggle.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(intermediate?.textContent).toContain("running: pnpm build");
|
||||||
|
expect(intermediate?.textContent).toContain("writing brand-guide.html");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("auto-collapses and hides after all steps complete", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const completedPlan = {
|
||||||
|
block: {
|
||||||
|
kind: "plan" as const,
|
||||||
|
planId: "p-done",
|
||||||
|
title: "Branding-Konzept finalisieren",
|
||||||
|
status: "completed" as const,
|
||||||
|
steps: [
|
||||||
|
{ id: "s1", label: "Slogans erstellen", status: "completed" as const },
|
||||||
|
{ id: "s2", label: "Brand Guide erstellen", status: "completed" as const },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
running: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
|
<ComposerTaskPill activePlan={completedPlan} />
|
||||||
|
</I18nProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('[data-testid="composer-task-pill"]')).not.toBeNull();
|
||||||
|
expect(
|
||||||
|
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
||||||
|
).toBe("Done");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
vi.advanceTimersByTime(3600);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('[data-testid="composer-task-pill"]')).toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PlanCard embeds intermediate steps under the active step", async () => {
|
||||||
|
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
|
<PlanCard block={state.block} steps={state.steps} />
|
||||||
|
</I18nProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeStepRow = container.querySelector('[data-testid="plan-step-s2"]');
|
||||||
|
expect(activeStepRow).not.toBeNull();
|
||||||
|
expect(activeStepRow?.querySelector('[data-testid="plan-intermediate-steps"]')).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -5,11 +5,14 @@ import { ATTACHMENT_ALLOWED_MIME_TYPES } from "@rakazo/contracts";
|
|||||||
import type { ComposerMention, SlashActionId } from "@rakazo/core";
|
import type { ComposerMention, SlashActionId } from "@rakazo/core";
|
||||||
import {
|
import {
|
||||||
clampMentionHighlightIndex,
|
clampMentionHighlightIndex,
|
||||||
|
clearComposerDraft,
|
||||||
mentionChipKey,
|
mentionChipKey,
|
||||||
|
readComposerDraft,
|
||||||
resolveMentionPickerKey,
|
resolveMentionPickerKey,
|
||||||
SLASH_ACTIONS,
|
SLASH_ACTIONS,
|
||||||
serializeComposerPrompt,
|
serializeComposerPrompt,
|
||||||
truncateSlashDescription,
|
truncateSlashDescription,
|
||||||
|
writeComposerDraft,
|
||||||
} from "@rakazo/core";
|
} from "@rakazo/core";
|
||||||
import {
|
import {
|
||||||
BotAvatar,
|
BotAvatar,
|
||||||
@ -28,6 +31,7 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
|
RotateCcw,
|
||||||
Settings,
|
Settings,
|
||||||
Square,
|
Square,
|
||||||
X,
|
X,
|
||||||
@ -37,6 +41,8 @@ import { memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } fr
|
|||||||
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
|
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
|
||||||
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
|
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
|
||||||
import { AutopilotDialog } from "./autopilot-dialog";
|
import { AutopilotDialog } from "./autopilot-dialog";
|
||||||
|
import type { ComposerActivePlan } from "./message-cards";
|
||||||
|
import { ComposerTaskPill } from "./message-cards";
|
||||||
import { previewMessageText } from "./message-preview";
|
import { previewMessageText } from "./message-preview";
|
||||||
import type { PendingAttachment } from "./shell-types";
|
import type { PendingAttachment } from "./shell-types";
|
||||||
import { FALLBACK_BOT_COLOR } from "./shell-types";
|
import { FALLBACK_BOT_COLOR } from "./shell-types";
|
||||||
@ -48,6 +54,7 @@ const addButtonClass =
|
|||||||
"tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer";
|
"tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer";
|
||||||
|
|
||||||
export const Composer = memo(function Composer({
|
export const Composer = memo(function Composer({
|
||||||
|
draftKey,
|
||||||
running,
|
running,
|
||||||
disabled,
|
disabled,
|
||||||
pendingAttachments,
|
pendingAttachments,
|
||||||
@ -76,7 +83,12 @@ export const Composer = memo(function Composer({
|
|||||||
onSlashAction,
|
onSlashAction,
|
||||||
onConnectModel,
|
onConnectModel,
|
||||||
onAutopilot,
|
onAutopilot,
|
||||||
|
activePlan,
|
||||||
|
stopped = false,
|
||||||
|
onResume,
|
||||||
|
onDismissStopped,
|
||||||
}: {
|
}: {
|
||||||
|
draftKey?: string;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
pendingAttachments: PendingAttachment[];
|
pendingAttachments: PendingAttachment[];
|
||||||
@ -107,15 +119,43 @@ export const Composer = memo(function Composer({
|
|||||||
onConnectModel?: () => void;
|
onConnectModel?: () => void;
|
||||||
/** Hand the order to the autopilot; the composer menu offers it only when set. */
|
/** Hand the order to the autopilot; the composer menu offers it only when set. */
|
||||||
onAutopilot?: (goal: string, until?: string) => Promise<void>;
|
onAutopilot?: (goal: string, until?: string) => Promise<void>;
|
||||||
|
activePlan?: ComposerActivePlan | null;
|
||||||
|
stopped?: boolean;
|
||||||
|
onResume?: () => void | Promise<void>;
|
||||||
|
onDismissStopped?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [draft, setDraft] = useState("");
|
const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]);
|
||||||
|
const [draft, setDraft] = useState(() => initialDraft.text);
|
||||||
const [autopilotOpen, setAutopilotOpen] = useState(false);
|
const [autopilotOpen, setAutopilotOpen] = useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||||
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
|
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
|
||||||
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
||||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null);
|
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
|
||||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]);
|
() => initialDraft.selectedSkill,
|
||||||
|
);
|
||||||
|
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>(
|
||||||
|
() => initialDraft.selectedMentions,
|
||||||
|
);
|
||||||
|
const loadedDraftKeyRef = useRef(draftKey);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (loadedDraftKeyRef.current !== draftKey) {
|
||||||
|
loadedDraftKeyRef.current = draftKey;
|
||||||
|
const stored = readComposerDraft(draftKey);
|
||||||
|
setDraft(stored.text);
|
||||||
|
setSelectedSkill(stored.selectedSkill);
|
||||||
|
setSelectedMentions(stored.selectedMentions);
|
||||||
|
setMentionQuery(null);
|
||||||
|
setMentionHighlightIndex(0);
|
||||||
|
setSlashQuery(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeComposerDraft(draftKey, {
|
||||||
|
text: draft,
|
||||||
|
selectedSkill,
|
||||||
|
selectedMentions,
|
||||||
|
});
|
||||||
|
}, [draft, draftKey, selectedMentions, selectedSkill]);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const runErrorRef = useRef<HTMLDivElement>(null);
|
const runErrorRef = useRef<HTMLDivElement>(null);
|
||||||
const submitFingerprint = useRef<string | null>(null);
|
const submitFingerprint = useRef<string | null>(null);
|
||||||
@ -265,6 +305,11 @@ export const Composer = memo(function Composer({
|
|||||||
function runSlashAction(action: SlashActionId) {
|
function runSlashAction(action: SlashActionId) {
|
||||||
setDraft("");
|
setDraft("");
|
||||||
setSlashQuery(null);
|
setSlashQuery(null);
|
||||||
|
writeComposerDraft(draftKey, {
|
||||||
|
text: "",
|
||||||
|
selectedSkill,
|
||||||
|
selectedMentions,
|
||||||
|
});
|
||||||
onSlashAction?.(action);
|
onSlashAction?.(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +345,10 @@ export const Composer = memo(function Composer({
|
|||||||
const slashSkillOptions = useMemo(() => {
|
const slashSkillOptions = useMemo(() => {
|
||||||
if (slashQuery === null) return [];
|
if (slashQuery === null) return [];
|
||||||
const query = slashQuery.trim().toLowerCase();
|
const query = slashQuery.trim().toLowerCase();
|
||||||
const skills = agentSkills ?? [];
|
// The user's own skills come first so built-in ones never crowd them out of the picker.
|
||||||
|
const skills = [...(agentSkills ?? [])].sort(
|
||||||
|
(a, b) => Number(a.source !== "user") - Number(b.source !== "user"),
|
||||||
|
);
|
||||||
return skills
|
return skills
|
||||||
.filter((skill) => {
|
.filter((skill) => {
|
||||||
if (!query) return true;
|
if (!query) return true;
|
||||||
@ -331,6 +379,7 @@ export const Composer = memo(function Composer({
|
|||||||
);
|
);
|
||||||
if (submitFingerprint.current === fingerprint) return;
|
if (submitFingerprint.current === fingerprint) return;
|
||||||
submitFingerprint.current = fingerprint;
|
submitFingerprint.current = fingerprint;
|
||||||
|
clearComposerDraft(draftKey);
|
||||||
setDraft("");
|
setDraft("");
|
||||||
setMentionQuery(null);
|
setMentionQuery(null);
|
||||||
setMentionHighlightIndex(0);
|
setMentionHighlightIndex(0);
|
||||||
@ -417,11 +466,11 @@ export const Composer = memo(function Composer({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
className={`relative z-30 m-0 min-w-0 border-0 px-3 pb-4 pt-3 md:px-6 md:pb-6 ${
|
className={`composer-bottom-fade pointer-events-none absolute inset-x-0 bottom-0 z-30 m-0 min-w-0 max-w-full overflow-x-hidden border-0 px-3 pb-4 pt-10 md:px-6 md:pb-6 md:pt-12 ${
|
||||||
draggingFiles ? "rounded-[14px] ring-2 ring-inset ring-ring" : ""
|
draggingFiles ? "rounded-[14px] ring-2 ring-inset ring-ring" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto w-full max-w-4xl">
|
<div className="pointer-events-auto mx-auto w-full max-w-4xl">
|
||||||
{onConnectModel ? (
|
{onConnectModel ? (
|
||||||
<div
|
<div
|
||||||
data-testid="composer-model-needed"
|
data-testid="composer-model-needed"
|
||||||
@ -442,6 +491,18 @@ export const Composer = memo(function Composer({
|
|||||||
className="mb-3 flex items-center gap-2 rounded-[14px] border border-destructive/40 bg-destructive/10 px-4 py-2 text-ui-body-sm text-destructive"
|
className="mb-3 flex items-center gap-2 rounded-[14px] border border-destructive/40 bg-destructive/10 px-4 py-2 text-ui-body-sm text-destructive"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 flex-1">{sendError ?? runError}</span>
|
<span className="min-w-0 flex-1">{sendError ?? runError}</span>
|
||||||
|
{runError && !sendError && onResume ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="composer-error-resume"
|
||||||
|
disabled={sending}
|
||||||
|
onClick={() => void onResume()}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded-full border border-destructive/30 bg-background/80 px-2.5 py-1 text-ui-label font-medium text-foreground transition-colors hover:bg-background disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RotateCcw className="size-3" aria-hidden="true" />
|
||||||
|
<Trans>Resume</Trans>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Dismiss error`}
|
aria-label={t`Dismiss error`}
|
||||||
@ -455,6 +516,39 @@ export const Composer = memo(function Composer({
|
|||||||
<X strokeWidth={2} className="size-icon-sm" />
|
<X strokeWidth={2} className="size-icon-sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : stopped && !running && onResume ? (
|
||||||
|
<div
|
||||||
|
data-testid="composer-stopped"
|
||||||
|
className="mb-3 flex items-center gap-2 rounded-[14px] border border-border bg-muted px-4 py-2 text-ui-body-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<Trans>Stopped</Trans>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="composer-resume"
|
||||||
|
disabled={sending}
|
||||||
|
onClick={() => void onResume()}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border bg-background px-2.5 py-1 text-ui-label font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RotateCcw className="size-3" aria-hidden="true" />
|
||||||
|
<Trans>Resume</Trans>
|
||||||
|
</button>
|
||||||
|
{onDismissStopped ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t`Dismiss`}
|
||||||
|
data-testid="composer-stopped-dismiss"
|
||||||
|
onClick={() => {
|
||||||
|
onDismissStopped();
|
||||||
|
window.requestAnimationFrame(() => textareaRef.current?.focus());
|
||||||
|
}}
|
||||||
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X strokeWidth={2} className="size-icon-sm" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{replyTarget ? (
|
{replyTarget ? (
|
||||||
<div
|
<div
|
||||||
@ -564,7 +658,7 @@ export const Composer = memo(function Composer({
|
|||||||
{showSlashPicker ? (
|
{showSlashPicker ? (
|
||||||
<div
|
<div
|
||||||
data-testid="slash-picker"
|
data-testid="slash-picker"
|
||||||
className="mb-2 overflow-hidden rounded-[14px] border border-border bg-muted"
|
className="mb-2 max-h-64 overflow-y-auto rounded-[14px] border border-border bg-muted"
|
||||||
>
|
>
|
||||||
{slashSkillOptions.map((skill) => (
|
{slashSkillOptions.map((skill) => (
|
||||||
<button
|
<button
|
||||||
@ -608,6 +702,7 @@ export const Composer = memo(function Composer({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{activePlan ? <ComposerTaskPill activePlan={activePlan} onResume={onResume} /> : null}
|
||||||
<div
|
<div
|
||||||
data-testid="composer-bar"
|
data-testid="composer-bar"
|
||||||
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200"
|
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200"
|
||||||
@ -665,6 +760,11 @@ export const Composer = memo(function Composer({
|
|||||||
onStart={async (goal, until) => {
|
onStart={async (goal, until) => {
|
||||||
await onAutopilot(goal, until);
|
await onAutopilot(goal, until);
|
||||||
setDraft("");
|
setDraft("");
|
||||||
|
writeComposerDraft(draftKey, {
|
||||||
|
text: "",
|
||||||
|
selectedSkill,
|
||||||
|
selectedMentions,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
|
||||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
|
||||||
import { Maximize2 } from "lucide-react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import {
|
|
||||||
ComputersUnavailableHint,
|
|
||||||
computersAreUnavailable,
|
|
||||||
} from "../../components/ComputersUnavailableHint";
|
|
||||||
import { AgentCursor } from "../../components/computer/AgentCursor";
|
|
||||||
import {
|
|
||||||
computerLabel,
|
|
||||||
computerPlaceholder,
|
|
||||||
DesktopKindEmptyState,
|
|
||||||
screenIframeSandbox,
|
|
||||||
} from "./computer-screen";
|
|
||||||
import { WorkspaceFilesView } from "./workspace-files";
|
|
||||||
|
|
||||||
/** The bot's screen in the side panel: a live, non-interactive preview that opens full window. */
|
|
||||||
export function ComputerPanelPreview({
|
|
||||||
active,
|
|
||||||
computer,
|
|
||||||
computerOpen,
|
|
||||||
booting,
|
|
||||||
embeddedScreenUrl,
|
|
||||||
computerScreenError,
|
|
||||||
sandboxProvider,
|
|
||||||
onOpen,
|
|
||||||
}: {
|
|
||||||
active: Bot;
|
|
||||||
computer: ComputerStatus | null;
|
|
||||||
computerOpen: boolean;
|
|
||||||
booting: boolean;
|
|
||||||
embeddedScreenUrl: string | null;
|
|
||||||
computerScreenError: ReactNode;
|
|
||||||
sandboxProvider: string | undefined;
|
|
||||||
onOpen: () => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useLingui();
|
|
||||||
// Opening the panel never starts the computer, so a resting one shows its Open action outright.
|
|
||||||
const resting =
|
|
||||||
!booting &&
|
|
||||||
computer?.state !== "running" &&
|
|
||||||
computer?.state !== "booting" &&
|
|
||||||
!computersAreUnavailable(sandboxProvider);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
data-testid="computer-preview"
|
|
||||||
className="group relative aspect-[16/10] overflow-hidden rounded-[14px] bg-background"
|
|
||||||
>
|
|
||||||
{computerOpen ? (
|
|
||||||
<div className="grid h-full place-items-center text-sm text-muted-foreground/80">
|
|
||||||
<Trans>Open in full window</Trans>
|
|
||||||
</div>
|
|
||||||
) : computer?.kind === "desktop" ? (
|
|
||||||
<DesktopKindEmptyState className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80" />
|
|
||||||
) : computer?.state === "running" && embeddedScreenUrl && !computerScreenError ? (
|
|
||||||
<>
|
|
||||||
<iframe
|
|
||||||
title={t`Bot screen preview`}
|
|
||||||
src={embeddedScreenUrl}
|
|
||||||
sandbox={screenIframeSandbox(embeddedScreenUrl)}
|
|
||||||
className="h-full w-full border-0 bg-black"
|
|
||||||
allow="clipboard-read; clipboard-write"
|
|
||||||
style={{ pointerEvents: "none" }}
|
|
||||||
/>
|
|
||||||
<AgentCursor botId={active.id} color={active.color} name={active.name} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80">
|
|
||||||
{computerScreenError ??
|
|
||||||
(computersAreUnavailable(sandboxProvider) ? (
|
|
||||||
<ComputersUnavailableHint />
|
|
||||||
) : (
|
|
||||||
computerPlaceholder(
|
|
||||||
computer?.state,
|
|
||||||
booting,
|
|
||||||
computerLabel(computer?.mode, active.name),
|
|
||||||
)
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!computerScreenError ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-testid="computer-preview-open"
|
|
||||||
data-resting={resting ? "" : undefined}
|
|
||||||
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-4 data-resting:opacity-100"
|
|
||||||
aria-label={t`Open`}
|
|
||||||
onClick={onOpen}
|
|
||||||
>
|
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-overlay px-3.5 py-2 text-ui-body text-foreground shadow-md">
|
|
||||||
<Maximize2 strokeWidth={1.9} aria-hidden className="size-icon-md" />
|
|
||||||
<Trans>Open</Trans>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 truncate text-ui-body text-muted-foreground" dir="auto">
|
|
||||||
{t`${active.name}'s screen`}
|
|
||||||
</p>
|
|
||||||
<WorkspaceFilesView botId={active.id} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -108,14 +108,20 @@ export function NewSpaceDialog({
|
|||||||
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
|
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className={isProject ? "max-h-[90vh] overflow-y-auto max-w-xl" : undefined}
|
className={
|
||||||
|
isProject
|
||||||
|
? "flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader
|
||||||
<DialogTitle className="flex items-center gap-2.5">
|
className={isProject ? "shrink-0 border-b border-border px-5 py-4" : undefined}
|
||||||
|
>
|
||||||
|
<DialogTitle className="flex min-w-0 items-center gap-2.5">
|
||||||
{isProject ? (
|
{isProject ? (
|
||||||
<>
|
<>
|
||||||
<ProjectIcon icon={icon} color={color} size={30} />
|
<ProjectIcon icon={icon} color={color} size={30} />
|
||||||
<span>
|
<span className="truncate">
|
||||||
<Trans>New project</Trans>
|
<Trans>New project</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
@ -133,7 +139,13 @@ export function NewSpaceDialog({
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 pt-1">
|
<div
|
||||||
|
className={
|
||||||
|
isProject
|
||||||
|
? "rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4"
|
||||||
|
: "min-w-0 space-y-4 pt-1"
|
||||||
|
}
|
||||||
|
>
|
||||||
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
||||||
<Trans>Name</Trans>
|
<Trans>Name</Trans>
|
||||||
<Input
|
<Input
|
||||||
@ -179,7 +191,7 @@ export function NewSpaceDialog({
|
|||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label={t`Icon`}
|
aria-label={t`Icon`}
|
||||||
className="grid max-h-36 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1"
|
className="grid max-h-36 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
|
||||||
>
|
>
|
||||||
{PROJECT_ICONS.map((option) => {
|
{PROJECT_ICONS.map((option) => {
|
||||||
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
||||||
@ -304,8 +316,12 @@ export function NewSpaceDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
|
{error ? (
|
||||||
<DialogFooter className="mt-4">
|
<p className={`text-ui-body text-destructive ${isProject ? "px-5 pt-2" : "mt-2"}`}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter className={isProject ? "mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5" : "mt-4"}>
|
||||||
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -84,11 +84,11 @@ export function FullscreenComputerOverlay({
|
|||||||
)}
|
)}
|
||||||
{!recordingSkill && hasControl ? (
|
{!recordingSkill && hasControl ? (
|
||||||
computer?.takeoverRequested ? (
|
computer?.takeoverRequested ? (
|
||||||
<span className="rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning">
|
<span className="shrink-0 whitespace-nowrap rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning">
|
||||||
<Trans>Needs you</Trans>
|
<Trans>Needs you</Trans>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success">
|
<span className="shrink-0 whitespace-nowrap rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success">
|
||||||
<Trans>You have control</Trans>
|
<Trans>You have control</Trans>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -95,16 +95,16 @@ export function ProjectLookDialog({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
data-testid="project-look-dialog"
|
data-testid="project-look-dialog"
|
||||||
className="max-h-[90vh] overflow-y-auto max-w-xl"
|
className="flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader className="shrink-0 border-b border-border px-5 py-4">
|
||||||
<DialogTitle className="flex items-center gap-3">
|
<DialogTitle className="flex min-w-0 items-center gap-3">
|
||||||
<ProjectIcon icon={icon} color={color} size={36} />
|
<ProjectIcon icon={icon} color={color} size={36} />
|
||||||
<span className="min-w-0 truncate">{name.trim() || initial.name}</span>
|
<span className="min-w-0 truncate">{name.trim() || initial.name}</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 pt-1">
|
<div className="rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
|
||||||
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
||||||
<Trans>Name</Trans>
|
<Trans>Name</Trans>
|
||||||
<Input
|
<Input
|
||||||
@ -147,7 +147,7 @@ export function ProjectLookDialog({
|
|||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label={t`Icon`}
|
aria-label={t`Icon`}
|
||||||
className="grid max-h-40 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1"
|
className="grid max-h-40 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
|
||||||
>
|
>
|
||||||
{PROJECT_ICONS.map((option) => {
|
{PROJECT_ICONS.map((option) => {
|
||||||
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
||||||
@ -243,8 +243,8 @@ export function ProjectLookDialog({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
|
{error ? <p className="px-5 pt-2 text-ui-body text-destructive">{error}</p> : null}
|
||||||
<DialogFooter className="mt-4">
|
<DialogFooter className="mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5">
|
||||||
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user