Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -134,6 +134,10 @@ describe("create-page executor type registry", () => {
|
||||
|
||||
describe("getExecutorDescription", () => {
|
||||
it.each([
|
||||
// `local` is the SEEDED Executor.type and reached this helper unmapped
|
||||
// before #2249's follow-up, so a local profile's header read "Custom
|
||||
// executor." Both spellings must resolve to the same sentence.
|
||||
["local", "Runs agents directly in the repository folder."],
|
||||
["local_pc", "Runs agents directly in the repository folder."],
|
||||
["worktree", "Creates git worktrees for isolated agent sessions."],
|
||||
["local_docker", "Runs Docker containers on this machine."],
|
||||
|
||||
@@ -10,7 +10,10 @@ import type { ExecutorType } from "@/lib/types/http";
|
||||
* create page — see the guard comment in eslint.i18n.options.mjs.
|
||||
*/
|
||||
export function getExecutorDescription(type: ExecutorType): string {
|
||||
if (type === "local_pc") return t("executors:descriptionLocalPc");
|
||||
// Both spellings reach this helper: `local` is the seeded `Executor.type`
|
||||
// (task/repository/sqlite/defaults.go) and `local_pc` comes from the Office
|
||||
// meta listing and the SQLite backfill — see lib/types/executor.ts.
|
||||
if (type === "local" || type === "local_pc") return t("executors:descriptionLocalPc");
|
||||
if (type === "worktree") return t("executors:descriptionWorktree");
|
||||
if (type === "local_docker") return t("executors:descriptionLocalDocker");
|
||||
if (type === "remote_docker") return t("executors:descriptionRemoteDocker");
|
||||
|
||||
@@ -34,7 +34,7 @@ export function PluginManifestCard({ plugin }: { plugin: PluginRecord }) {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
{/* Row VALUES are manifest data — the plugin id, its version, its
|
||||
author — and are never translated. Only the row labels are copy. */}
|
||||
<ManifestRow label="ID" value={plugin.id} mono />
|
||||
<ManifestRow label={t("plugins:manifestId")} value={plugin.id} mono />
|
||||
<ManifestRow label={t("plugins:manifestVersion")} value={plugin.version} mono />
|
||||
<ManifestRow label={t("plugins:manifestApiVersion")} value={String(plugin.api_version)} />
|
||||
<ManifestRow label={t("plugins:manifestAuthor")} value={plugin.author || "—"} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState, memo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconMessageCircle, IconPlus } from "@tabler/icons-react";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@kandev/ui/sheet";
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@kandev/ui/drawer";
|
||||
@@ -114,6 +115,8 @@ export function MobileTaskList({
|
||||
const collapsedSubtaskParents = useAppStore((s) => s.collapsedSubtaskParents);
|
||||
const toggleSubtaskCollapsed = useAppStore((s) => s.toggleSubtaskCollapsed);
|
||||
const handleToggleGroup = useSidebarGroupToggle(view.id);
|
||||
// See useGroupedSidebarView: the executorType group label is catalog-backed.
|
||||
const { i18n } = useTranslation();
|
||||
const grouped = useMemo(
|
||||
() =>
|
||||
applyView(tasks, view, {
|
||||
@@ -121,7 +124,7 @@ export function MobileTaskList({
|
||||
orderedTaskIds,
|
||||
subtaskOrderByParentId,
|
||||
}),
|
||||
[tasks, view, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId],
|
||||
[tasks, view, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId, i18n.language],
|
||||
);
|
||||
return (
|
||||
<TaskSwitcher
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
|
||||
import { StateProvider, useAppStoreApi } from "@/components/state-provider";
|
||||
import { activateLocale, i18n } from "@/lib/i18n";
|
||||
import { useFilterValueOptions } from "./use-filter-value-options";
|
||||
|
||||
function wrapper() {
|
||||
return function Wrapper({ children }: PropsWithChildren) {
|
||||
return <StateProvider>{children}</StateProvider>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `executorTypeOptions` labels its options through `getExecutorLabel`, which
|
||||
* reads the catalog at call time. The hook memoizes on store data, so without
|
||||
* the active language in its dependency array the labels would keep the
|
||||
* previous locale until a task snapshot happened to change — invisible to the
|
||||
* pseudo-locale, because the text IS translated, just stale.
|
||||
*
|
||||
* The store deliberately stays untouched across the switch: that is the whole
|
||||
* point. If the only thing that changed is the locale and the label follows,
|
||||
* the dependency is doing its job.
|
||||
*/
|
||||
describe("useFilterValueOptions — locale", () => {
|
||||
afterAll(async () => {
|
||||
if (i18n.language !== "en") await activateLocale("en");
|
||||
});
|
||||
|
||||
it("recomputes executor-type labels on a locale switch with unchanged store data", async () => {
|
||||
await activateLocale("en");
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const store = useAppStoreApi();
|
||||
return { options: useFilterValueOptions("executorType"), store };
|
||||
},
|
||||
{ wrapper: wrapper() },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.store.getState().hydrate({
|
||||
kanbanMulti: {
|
||||
snapshots: {
|
||||
w1: {
|
||||
workflowId: "w1",
|
||||
workflowName: "W1",
|
||||
steps: [],
|
||||
tasks: [{ id: "t1", primaryExecutorType: "local" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
});
|
||||
|
||||
const before = result.current.options;
|
||||
expect(before.map((o) => o.value)).toEqual(["local"]);
|
||||
expect(before.map((o) => o.label)).toEqual(["Local"]);
|
||||
|
||||
await act(async () => {
|
||||
await activateLocale("pseudo");
|
||||
});
|
||||
|
||||
const after = result.current.options;
|
||||
// Same option VALUE — it is the persisted executor type, never translated.
|
||||
expect(after.map((o) => o.value)).toEqual(["local"]);
|
||||
expect(after[0].label).not.toBe("Local");
|
||||
expect(after[0].label).toBe(i18n.t("executors:typeLocal"));
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { applyView } from "@/lib/sidebar/apply-view";
|
||||
import { useEffectiveSidebarView } from "@/hooks/domains/sidebar/use-effective-sidebar-view";
|
||||
import { useSidebarTaskPrefs } from "@/hooks/domains/sidebar/use-sidebar-task-prefs";
|
||||
@@ -10,6 +11,10 @@ export function useGroupedSidebarView(displayTasks: TaskSwitcherItem[]) {
|
||||
const prefs = useSidebarTaskPrefs();
|
||||
const effectiveView = useEffectiveSidebarView();
|
||||
const { pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId } = prefs;
|
||||
// `applyGroup`'s executorType label comes from `getExecutorLabel`, which reads
|
||||
// the catalog. Without the language in the deps the group heading keeps the
|
||||
// previous locale until task data changes.
|
||||
const { i18n } = useTranslation();
|
||||
const grouped = useMemo(
|
||||
() =>
|
||||
applyView(displayTasks, effectiveView, {
|
||||
@@ -17,7 +22,14 @@ export function useGroupedSidebarView(displayTasks: TaskSwitcherItem[]) {
|
||||
orderedTaskIds,
|
||||
subtaskOrderByParentId,
|
||||
}),
|
||||
[displayTasks, effectiveView, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId],
|
||||
[
|
||||
displayTasks,
|
||||
effectiveView,
|
||||
pinnedTaskIds,
|
||||
orderedTaskIds,
|
||||
subtaskOrderByParentId,
|
||||
i18n.language,
|
||||
],
|
||||
);
|
||||
return { grouped, effectiveView, prefs };
|
||||
}
|
||||
|
||||
@@ -1311,13 +1311,21 @@ export const i18nGuardFiles = [
|
||||
//
|
||||
// `getExecutorLabel` returns catalog text now, so a caller that does not
|
||||
// subscribe to i18n renders the previous locale until something else makes it
|
||||
// re-render. Every consumer this PR owns subscribes, and
|
||||
// `use-filter-value-options.ts` (the task sidebar's executor filter) gained
|
||||
// `i18n.language` in its memo deps for the same reason. One consumer is left:
|
||||
// `lib/sidebar/apply-view.ts` builds a group label from it inside the sidebar
|
||||
// view pipeline, which this PR does not own — its labels resolve correctly on
|
||||
// load and lag a runtime locale switch until the view recomputes. It belongs
|
||||
// to whoever migrates `lib/sidebar`.
|
||||
// re-render. All three consumers are covered: `ProfileCard` subscribes,
|
||||
// `use-filter-value-options.ts` (the task sidebar's executor filter) carries
|
||||
// `i18n.language` in its memo deps, and both `applyView` consumers — the
|
||||
// desktop sidebar and the mobile task-switcher sheet — do the same for
|
||||
// `applyGroup`'s executorType heading in `lib/sidebar/apply-view.ts`.
|
||||
//
|
||||
// This bug class is invisible to BOTH the guard and the pseudo-locale: the
|
||||
// text is translated, just frozen at the previous locale, so it renders
|
||||
// accented and reads as done. `check-module-scope-t.mjs` does not fire
|
||||
// either — the `t()` call sits inside a function, which is correct, since it
|
||||
// does resolve at call time. Only a locale-switch test covers it, which is
|
||||
// what `use-filter-value-options-locale.test.tsx` is for.
|
||||
//
|
||||
// Still English in `lib/sidebar/apply-view.ts` and belonging to whoever
|
||||
// migrates `lib/sidebar`: `UNASSIGNED_LABEL` and `MULTI_REPO_LABEL`.
|
||||
//
|
||||
// One deliberate English change, the only one in this PR: the SSH
|
||||
// running-sessions confirm read "This executor has 3 running session(s)."
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"manifestApiVersion": "API version",
|
||||
"manifestAuthor": "Author",
|
||||
"manifestCapabilities": "Capabilities",
|
||||
"manifestId": "ID",
|
||||
"manifestInstalled": "Installed",
|
||||
"manifestSigned": "Signed",
|
||||
"manifestVersion": "Version",
|
||||
@@ -74,7 +75,7 @@
|
||||
"selectPlaceholder": "Select...",
|
||||
"selectedUtilityAgentUnavailable": "Selected utility agent unavailable",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "Browse the marketplace, then install, enable, disable, and uninstall kandev plugins.",
|
||||
"settingsDescription": "Browse the marketplace, then install, enable, disable, and uninstall Kandev plugins.",
|
||||
"settingsSaved": "Plugin settings saved",
|
||||
"settingsSavedReloadFailed": "Settings saved, but reloading them failed — refresh to confirm.",
|
||||
"sortMostStars": "Most stars",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"manifestApiVersion": "ÀƤĨ vēŕśĩōń",
|
||||
"manifestAuthor": "Àũţĥōŕ",
|
||||
"manifestCapabilities": "Ćàƥàƀĩĺĩţĩēś",
|
||||
"manifestId": "ĨĎ",
|
||||
"manifestInstalled": "Ĩńśţàĺĺēď",
|
||||
"manifestSigned": "Śĩĝńēď",
|
||||
"manifestVersion": "Vēŕśĩōń",
|
||||
@@ -74,7 +75,7 @@
|
||||
"selectPlaceholder": "Śēĺēćţ...",
|
||||
"selectedUtilityAgentUnavailable": "Śēĺēćţēď ũţĩĺĩţŷ àĝēńţ ũńàvàĩĺàƀĺē",
|
||||
"settings": "Śēţţĩńĝś",
|
||||
"settingsDescription": "Ɓŕōŵśē ţĥē ḿàŕķēţƥĺàćē, ţĥēń ĩńśţàĺĺ, ēńàƀĺē, ďĩśàƀĺē, àńď ũńĩńśţàĺĺ ķàńďēv ƥĺũĝĩńś.",
|
||||
"settingsDescription": "Ɓŕōŵśē ţĥē ḿàŕķēţƥĺàćē, ţĥēń ĩńśţàĺĺ, ēńàƀĺē, ďĩśàƀĺē, àńď ũńĩńśţàĺĺ Ķàńďēv ƥĺũĝĩńś.",
|
||||
"settingsSaved": "Ƥĺũĝĩń śēţţĩńĝś śàvēď",
|
||||
"settingsSavedReloadFailed": "Śēţţĩńĝś śàvēď, ƀũţ ŕēĺōàďĩńĝ ţĥēḿ ƒàĩĺēď — ŕēƒŕēśĥ ţō ćōńƒĩŕḿ.",
|
||||
"sortMostStars": "Ḿōśţ śţàŕś",
|
||||
|
||||
Reference in New Issue
Block a user