From 347acf0171a3b6c349315f1a1fc8985af8716102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 03:10:29 +0100 Subject: [PATCH] Command HQ shows what plugins offer, and what has not been started MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the UI half. a row per plugin that ships commanders, below the owner's own categories and separated from them — because they are not the same kind of thing. a category is the owner's filing and they may rename it; a plugin is where something came from and they may not. mixed into one list, "av-club" reads as a folder somebody made, and the first thing you do with a folder you made is rename it. offers sit ABOVE the grid rather than in it. a commander in the grid runs, has a transcript and has cost money; an offer is a description on disk. drawn as the same card with a badge, the two look interchangeable, and the first consequence is somebody hunting for the transcript of a thing that has never run. an offer STAYS after it is used. it is a template and there is no reason to have only one — two folders watched for two formats is two commanders from one offer, and hiding it would make the second look impossible. the form is drawn from the template's own `inputs`. nothing in the component knows what av-club wants: options become buttons, boolean becomes yes/no, everything else is a field. the button names what is missing rather than marking it red. TWO REAL BUGS FOUND BY WIRING IT UP, both in the list route: `?category=plugin:av-club` reached `Number('plugin:av-club')` — NaN — and the query compared `category_id = NaN`. that matches nothing and looks exactly like "this plugin has no commanders", which is the worst way for a filter to fail. parsed before the numeric branch now. and the grid DROPS every row without a transcript. that was right when the only way to get a row was to start a conversation; it is wrong now, because a commander from a template arrives deliberately not running — so the thing somebody just created was invisible, under a toast saying it worked. a row with a `plugin` and no session is now projected from the row itself, with the zeroes that are true of it, and sorted FIRST: it is the one that needs a decision. ordinary rows keep the old behaviour, since a hand-made row with no transcript is a conversation that failed to start and the design refuses to draw a tombstone for it. four locale keys, all six languages, parity test green. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- .../officer_db/src/command-hq/queries.ts | 11 +- src/servers/api/command-hq/router.ts | 66 +++++- .../apps/CommandHQ/CommandHqCategories.tsx | 34 +++ .../src/apps/CommandHQ/CommandHqDetail.tsx | 14 +- .../src/apps/CommandHQ/CommandHqGrid.tsx | 7 + .../apps/CommandHQ/PluginCommanderOffers.tsx | 198 ++++++++++++++++++ .../src/apps/CommandHQ/command-hq-routes.ts | 3 + .../officerdev/src/apps/CommandHQ/types.ts | 32 ++- .../src/apps/CommandHQ/useCommandHq.ts | 34 +++ .../officerdev/src/locales/de-DE.json | 6 +- .../officerdev/src/locales/en-GB.json | 6 +- .../officerdev/src/locales/es-ES.json | 6 +- .../officerdev/src/locales/fr-FR.json | 6 +- .../officerdev/src/locales/pt-PT.json | 6 +- .../officerdev/src/locales/sv-SE.json | 6 +- 15 files changed, 412 insertions(+), 23 deletions(-) create mode 100644 src/workspaces/officerdev/src/apps/CommandHQ/PluginCommanderOffers.tsx diff --git a/src/databases/officer_db/src/command-hq/queries.ts b/src/databases/officer_db/src/command-hq/queries.ts index f1537040..703fce37 100644 --- a/src/databases/officer_db/src/command-hq/queries.ts +++ b/src/databases/officer_db/src/command-hq/queries.ts @@ -63,6 +63,8 @@ export type CommandHqSessionRow = { /** What the owner called it. Null for rows filed before the column existed — fall back to the transcript. */ name: string | null; categoryId: number | null; + plugin: string | null; + pluginConfig: Record | null; watcherArmed: boolean; watcherTaskId: string | null; watcherArmedAt: Date | null; @@ -78,6 +80,8 @@ const SESSION_ROW_COLUMNS = { referencePaths: commandHqSessions.referencePaths, name: commandHqSessions.name, categoryId: commandHqSessions.categoryId, + plugin: commandHqSessions.plugin, + pluginConfig: commandHqSessions.pluginConfig, watcherArmed: commandHqSessions.watcherArmed, watcherTaskId: commandHqSessions.watcherTaskId, watcherArmedAt: commandHqSessions.watcherArmedAt, @@ -166,9 +170,12 @@ export async function getCommandHqSessionById(userId: number, id: number): Promi export async function listCommandHqSessions( userId: number, categoryId?: number | null, + /** Instead of a category: only commanders shipped by this plugin. The two never combine. */ + plugin?: string, ): Promise { - const where = - categoryId === undefined + const where = plugin + ? and(eq(commandHqSessions.userId, userId), eq(commandHqSessions.plugin, plugin)) + : categoryId === undefined ? eq(commandHqSessions.userId, userId) : categoryId === null ? and(eq(commandHqSessions.userId, userId), isCategoryNull()) diff --git a/src/servers/api/command-hq/router.ts b/src/servers/api/command-hq/router.ts index 42e6eee9..3bdcd9c7 100644 --- a/src/servers/api/command-hq/router.ts +++ b/src/servers/api/command-hq/router.ts @@ -203,6 +203,17 @@ type CommanderSummary = ClaudeSessionSummary & { watcherLastFiredAt: Date | null; /** The running total — every turn this commander has ever cost, summed. See `GET .../turns` for the raw series. */ cost: CommandHqCostSummary; + /** Where it came from, when a plugin's template made it. Null for one somebody wrote themselves. */ + plugin?: string | null; + /** The owner's answers to that template's inputs, so the card can say what it is set to watch. */ + pluginConfig?: Record | null; + /** + * Created, never opened — so there is no transcript behind it and its id is not a session id. + * + * Only ever true for a plugin's commander. A hand-made row without a transcript is one whose + * conversation failed to start, and the design deliberately refuses to draw a tombstone for it. + */ + pending?: boolean; }; async function isRunning(sessionId: string): Promise { @@ -224,7 +235,15 @@ commandHqRouter.get('/commanders', async (ctx) => { const user = ctx.get('user'); const who = await commandHqIdentity(user); const categoryParam = ctx.req.query('category'); - const categoryId = categoryParam === undefined ? undefined : categoryParam === 'none' ? null : Number(categoryParam); + + /* + `plugin:` is a filter on provenance, not a category, and is parsed BEFORE the numeric branch. + Without this it reached `Number('plugin:av-club')` — NaN — and the query compared `category_id = NaN`, + which matches nothing and looks exactly like "this plugin has no commanders". + */ + const pluginFilter = categoryParam?.startsWith('plugin:') ? categoryParam.slice('plugin:'.length) : undefined; + const categoryId = + pluginFilter || categoryParam === undefined ? undefined : categoryParam === 'none' ? null : Number(categoryParam); /* Each commander's transcript is loaded BY ID, not found by scanning one directory — a commander's own @@ -237,7 +256,7 @@ commandHqRouter.get('/commanders', async (ctx) => { reason: there is no transcript to summarise. Filtered out before the lookup rather than after, so `loadClaudeSessionSummaryById` is never asked about an id that cannot exist. */ - const rows = await listCommandHqSessions(user.id, categoryId); + const rows = await listCommandHqSessions(user.id, categoryId, pluginFilter); const withTranscript = rows.filter((row) => row.sessionId !== null); const summaries = withTranscript .map((row) => loadClaudeSessionSummaryById(who, row.sessionId!)) @@ -260,6 +279,39 @@ commandHqRouter.get('/commanders', async (ctx) => { ), ]); + /* + A commander CREATED but never opened has no transcript, and every branch above drops it. + + That was right when the only way to get a row was to start a conversation. It is wrong now: a + commander made from a plugin's template arrives deliberately not running, and dropping it means the + thing somebody just created is invisible — with a toast saying it worked. So a row with a `plugin` + and no session is projected from the ROW alone, with the zeroes that are actually true of it. + + Ordinary commanders keep the old behaviour. A hand-made row with no transcript is a row whose + conversation failed to start, and a tombstone for it is what the design deliberately refused. + */ + const pending = rows + .filter((row) => row.sessionId === null && row.plugin) + .map( + (row) => + ({ + id: `pending-${row.id}`, + title: row.name ?? 'Commander', + dirName: row.dirName, + categoryId: row.categoryId, + tagIds: [], + running: false, + watcherArmed: false, + watcherArmedAt: null, + watcherStoppedAt: null, + watcherLastFiredAt: null, + cost: { turnCount: 0, inputTokens: 0, outputTokens: 0, totalUsd: 0 }, + plugin: row.plugin, + pluginConfig: row.pluginConfig, + pending: true, + }) as unknown as CommanderSummary, + ); + const commanders: CommanderSummary[] = filed .map((row) => ({ ...liveById.get(row.sessionId!)!, @@ -279,6 +331,10 @@ commandHqRouter.get('/commanders', async (ctx) => { // grid offered nothing to stop — which is the one thing this column exists to make possible. Keyed // on `dirName`, not the transcript id: the pid file lives in this commander's OWN `state/`, not // beside the transcript. + // Provenance, so the UI can put a plugin's commanders in their own section rather than inferring + // it from a category name somebody can rename. + plugin: row.plugin, + pluginConfig: row.pluginConfig, watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null, // Whether the LOOP is alive right now, separate from `running` (an agent turn in progress) and from // `watcherArmed` (which is also true for a row that says armed but whose loop has gone). @@ -290,7 +346,9 @@ commandHqRouter.get('/commanders', async (ctx) => { })) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); - return ctx.json({ commanders }); + // Pending ones FIRST: a commander that was just created and needs a decision is the thing somebody came + // to the screen for, and sorting it by `updatedAt` among running ones would bury it on a busy account. + return ctx.json({ commanders: [...pending, ...commanders] }); }); /** @@ -465,6 +523,8 @@ commandHqRouter.get('/commanders/:sessionId', async (ctx) => { categoryId: row.categoryId, tagIds: tagsByRow.get(row.id) ?? [], running, + plugin: row.plugin, + pluginConfig: row.pluginConfig, watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null, watcherRunning: liveLoopPid(who.email, row.dirName) !== null, watcherArmedAt: row.watcherArmedAt, diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx index 6f656eff..bee62e0a 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx @@ -4,6 +4,7 @@ import { Plus } from 'lucide-react'; import { toast } from '@/components/ui/sonner'; import { errorText } from 'helpers/error-text'; import { categoryFromParam, commandHqListPath } from './command-hq-routes'; +import { usePluginCommanderTemplates } from './useCommandHq'; import { useCommandHqCategories } from './useCommandHq'; import type { CommandHqCategoryFilter } from './types'; import { useTranslation } from '../../locales'; @@ -36,6 +37,11 @@ export const CommandHqCategories = () => { const { t } = useTranslation(); const [searchParams] = useSearchParams(); const active: CommandHqCategoryFilter = categoryFromParam(searchParams.get('category')); + + // Which plugins have anything to offer. Derived from the templates rather than from the installed + // plugin list: a plugin that ships no commander should not get a row that is always empty. + const { templates } = usePluginCommanderTemplates(); + const plugins = [...new Set(templates.map((template) => template.plugin))].sort(); const { categories, createCategory } = useCommandHqCategories(); const [adding, setAdding] = useState(false); const [name, setName] = useState(''); @@ -96,6 +102,34 @@ export const CommandHqCategories = () => { to={commandHqListPath(category.id)} /> ))} + + {/* + One row per plugin that offers commanders, below the owner's own categories and separated from + them. + + Separated because they are not the same kind of thing. A category is the owner's filing and they + can rename or delete it; a plugin is where something came from and they cannot. Mixing them into + one list would invite the reasonable assumption that "av-club" is a folder they made, and the + first thing somebody does with a folder they made is rename it. + + Shown even when the plugin has no commanders yet — that is the whole point. The row is how you + find out a plugin is offering one. + */} + {plugins.length > 0 && ( +
+ + {t('commandHq.fromPlugins')} + + {plugins.map((plugin) => ( + + ))} +
+ )} {adding && ( // No submit button — a form with exactly one text field still submits on Enter.
diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx index 651adc71..84907b76 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx @@ -20,7 +20,7 @@ import { useCommandHqCommanderActions, useCreateCommander, } from './useCommandHq'; -import type { CommandHqCommanderCreated } from './types'; +import type { CommandHqCommanderCreated, CommandHqCategoryFilter } from './types'; import { formatCommanderCost, formatTokens } from './format'; import { NEW_COMMANDER_GUIDE } from './newCommanderGuide'; import { useTranslation } from '../../locales'; @@ -246,7 +246,7 @@ function NewCommanderSetup({ category, onCreated, }: { - category: number | 'none' | undefined; + category: CommandHqCategoryFilter; onCreated: (commander: CommandHqCommanderCreated) => void; }) { /* @@ -335,7 +335,7 @@ function ActiveNewCommander({ onStarted, }: { commander: CommandHqCommanderCreated; - category: number | 'none' | undefined; + category: CommandHqCategoryFilter; onStarted: (id: string) => void; }) { const { t, tUi, locale } = useTranslation(); @@ -446,13 +446,7 @@ function ActiveNewCommander({ * version had (a `messages.length === 0` latch that could not tell "never started" from "started, and the * socket dropped"). */ -function NewCommander({ - category, - onStarted, -}: { - category: number | 'none' | undefined; - onStarted: (id: string) => void; -}) { +function NewCommander({ category, onStarted }: { category: CommandHqCategoryFilter; onStarted: (id: string) => void }) { const [commander, setCommander] = useState(null); if (!commander) return ; return ; diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqGrid.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqGrid.tsx index 1438e0ff..b244393a 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqGrid.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqGrid.tsx @@ -5,6 +5,7 @@ import { Check, Plus, Trash2, X } from 'lucide-react'; import { toast } from '@/components/ui/sonner'; import { errorText } from 'helpers/error-text'; import { categoryFromParam, commandHqListPath, commandHqNewPath, commandHqSessionPath } from './command-hq-routes'; +import { PluginCommanderOffers } from './PluginCommanderOffers'; import { useCommandHqCommanderActions, useCommandHqCommanders } from './useCommandHq'; import { formatCommanderCost, formatTokens } from './format'; import type { CommandHqCategoryFilter, CommandHqCommander } from './types'; @@ -170,6 +171,12 @@ export const CommandHqGrid = () => { row. */}
+ {/* Only on a plugin's own filter. On "All commanders" this would put every plugin's offers above + a list that is mostly not theirs, which is a different screen pretending to be this one. */} + {typeof category === 'string' && category.startsWith('plugin:') && ( + + )} + {commanders.map((commander) => ( { + const { t } = useTranslation(); + const { templates, createFromTemplate } = usePluginCommanderTemplates(); + const [configuring, setConfiguring] = useState(null); + + const offers = templates.filter((template) => template.plugin === plugin); + if (offers.length === 0) return null; + + return ( +
+
+ + + {t('commandHq.available')} + +
+ +
+ {offers.map((offer) => ( +
+
+
+

{offer.name}

+

{offer.description}

+ {offer.watches &&

{offer.watches}

} +
+ +
+ + {configuring?.dirName === offer.dirName && ( + setConfiguring(null)} + onSubmit={(name, config) => { + createFromTemplate.mutate( + { plugin: offer.plugin, template: offer.dirName, name, config }, + { + onSuccess: () => { + setConfiguring(null); + // Named, because it is now in the grid below among things that look alike. + toast.success(`${name} created — it is not running yet`); + }, + onError: (err: unknown) => + toast.error(err instanceof Error ? err.message : 'could not create it'), + }, + ); + }} + /> + )} +
+ ))} +
+
+ ); +}; + +/** + * The template's own inputs, drawn from its declaration. + * + * Nothing here knows what av-club's commander wants. `options` becomes a select, `boolean` becomes + * yes/no, everything else is a text field — the same three shapes a task's inputs use, because they + * are the same declaration. + */ +const ConfigureForm = ({ + template, + busy, + onCancel, + onSubmit, +}: { + template: PluginCommanderTemplate; + busy: boolean; + onCancel: () => void; + onSubmit: (name: string, config: Record) => void; +}) => { + const { t } = useTranslation(); + const [name, setName] = useState(template.name); + const [values, setValues] = useState>(() => + Object.fromEntries(Object.entries(template.inputs).map(([key, spec]) => [key, spec.default ?? ''])), + ); + + const missing = Object.entries(template.inputs) + .filter(([key, spec]) => spec.required && !values[key]?.trim()) + .map(([key]) => key); + + return ( + { + event.preventDefault(); + if (missing.length === 0 && name.trim()) onSubmit(name.trim(), values); + }} + > + + + {Object.entries(template.inputs).map(([key, spec]) => ( + + ))} + +
+ + + {/* Named, so it is obvious WHICH field is stopping the button rather than hunting for a red mark. */} + {missing.length > 0 && ( + needs {missing.join(', ').replace(/_/g, ' ')} + )} +
+ + ); +}; diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/command-hq-routes.ts b/src/workspaces/officerdev/src/apps/CommandHQ/command-hq-routes.ts index 2f25d12f..beacecc4 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/command-hq-routes.ts +++ b/src/workspaces/officerdev/src/apps/CommandHQ/command-hq-routes.ts @@ -40,6 +40,9 @@ export const commandHqSessionPath = (sessionId: string, category?: CommandHqCate export const categoryFromParam = (raw: string | null): CommandHqCategoryFilter => { if (raw === null) return undefined; if (raw === 'none') return 'none'; + // `plugin:av-club`. Checked before the numeric parse, since an appName never parses as a number and + // would otherwise fall through to `undefined` and silently show every commander. + if (raw.startsWith('plugin:') && raw.length > 'plugin:'.length) return raw as CommandHqCategoryFilter; const id = Number(raw); return Number.isFinite(id) ? id : undefined; }; diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/types.ts b/src/workspaces/officerdev/src/apps/CommandHQ/types.ts index 58d642c9..898adb66 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/types.ts +++ b/src/workspaces/officerdev/src/apps/CommandHQ/types.ts @@ -26,8 +26,16 @@ export type CommandHqCostSummary = { totalUsd: number; }; -/** `category` query value: omitted (all), `'none'` (uncategorised), or a category id. */ -export type CommandHqCategoryFilter = number | 'none' | undefined; +/** + * `category` query value: omitted (all), `'none'` (uncategorised), a category id, or `plugin:`. + * + * The plugin form is not a category and deliberately does not pretend to be one. A commander's category + * is the owner's filing; which plugin shipped it is a fact about where it came from, and the two are + * independent — a plugin's commander can also be filed under "Nightly" without either meaning changing. + * One query parameter carries both because they are both "which subset of the grid", and that is what + * this parameter has always meant. + */ +export type CommandHqCategoryFilter = number | 'none' | `plugin:${string}` | undefined; /** One grid card — `ClaudeSessionSummary` plus what Command HQ knows about it. See `GET /commanders`. */ export type CommandHqCommander = { @@ -100,3 +108,23 @@ export type CommandHqFire = { taskId: string; firedAt: string; }; + +/** + * A commander a plugin OFFERS but which does not exist yet. + * + * Deliberately a different type from `CommandHqCommander`, and fetched from a different endpoint. One + * runs, has a transcript and costs money; the other is a description on disk. Modelling them as one + * type with an `exists` flag would mean every consumer checking the flag before touching anything, and + * the one that forgets renders a card with a cost of zero for something that was never started. + */ +export type PluginCommanderTemplate = { + dirName: string; + plugin: string; + name: string; + description: string; + watches?: string; + inputs: Record< + string, + { type: string; description?: string; default?: string; options?: string[]; required?: boolean } + >; +}; diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/useCommandHq.ts b/src/workspaces/officerdev/src/apps/CommandHQ/useCommandHq.ts index 1e44e447..5cba14ba 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/useCommandHq.ts +++ b/src/workspaces/officerdev/src/apps/CommandHQ/useCommandHq.ts @@ -6,6 +6,7 @@ import type { CommandHqCommander, CommandHqCommanderCreated, CommandHqCommanderDetail, + PluginCommanderTemplate, } from './types'; const CATEGORIES_KEY = ['command-hq', 'categories'] as const; @@ -173,3 +174,36 @@ export function useCommandHqChildren(sessionId: string | undefined) { isLoading, }; } + +/** + * What the installed plugins offer, and making one real. + * + * The list is cheap and changes only when a plugin is installed, updated or disabled — so it is not + * refetched on a window focus. A plugin the owner just installed appears when Command HQ next mounts, + * which is the same moment its dock tile does. + */ +export const usePluginCommanderTemplates = () => { + const { get, post } = useClient(); + const qc = useQueryClient(); + + const { data, isLoading } = useQuery<{ templates: PluginCommanderTemplate[] }>({ + queryKey: ['command-hq', 'plugin-commanders'], + queryFn: () => get('/command-hq/plugin-commanders'), + staleTime: 60_000, + }); + + const createFromTemplate = useMutation({ + mutationFn: (input: { plugin: string; template: string; name: string; config: Record }) => + post('/command-hq/commanders', input), + // The grid reads `commanders`, and a newly created one has to appear in it without a reload — + // creating from a template is the one moment somebody is certain something should have happened. + onSuccess: () => qc.invalidateQueries({ queryKey: ['command-hq', 'commanders'] }), + }); + + return { templates: data?.templates ?? EMPTY_TEMPLATES, isLoading, createFromTemplate }; +}; + +// Module-level, so an empty result is the SAME array every render. A fresh `[]` in the fallback is a new +// identity each time, which re-runs every effect and memo that depends on it — the trap this file already +// avoids for categories and commanders. +const EMPTY_TEMPLATES: PluginCommanderTemplate[] = []; diff --git a/src/workspaces/officerdev/src/locales/de-DE.json b/src/workspaces/officerdev/src/locales/de-DE.json index e7735cf8..5aa115db 100644 --- a/src/workspaces/officerdev/src/locales/de-DE.json +++ b/src/workspaces/officerdev/src/locales/de-DE.json @@ -257,7 +257,11 @@ }, "startHint": "Er sieht sich zunächst an, worauf Sie ihn hingewiesen haben, und fragt Sie dann, worauf er achten soll.", "startBlocked": "{{missing}} — dann startet dies ihn.", - "confirmDeletingNamed": "Löschen von {{name}} bestätigen" + "confirmDeletingNamed": "Löschen von {{name}} bestätigen", + "fromPlugins": "Von Plugins", + "available": "Verfügbar", + "configureAndCreate": "Einrichten und erstellen", + "notCreatedYet": "Noch nicht erstellt — richten Sie es ein, um zu starten." }, "chatHistory": { "newChat": "Neuer Chat", diff --git a/src/workspaces/officerdev/src/locales/en-GB.json b/src/workspaces/officerdev/src/locales/en-GB.json index f28d3ff1..24545941 100644 --- a/src/workspaces/officerdev/src/locales/en-GB.json +++ b/src/workspaces/officerdev/src/locales/en-GB.json @@ -257,7 +257,11 @@ }, "startHint": "It will look at what you pointed it to, then ask you what to watch for.", "startBlocked": "{{missing}} — then this starts it.", - "confirmDeletingNamed": "Confirm deleting {{name}}" + "confirmDeletingNamed": "Confirm deleting {{name}}", + "fromPlugins": "From plugins", + "available": "Available", + "configureAndCreate": "Configure and create", + "notCreatedYet": "Not created yet — configure it to start." }, "chatHistory": { "newChat": "New Chat", diff --git a/src/workspaces/officerdev/src/locales/es-ES.json b/src/workspaces/officerdev/src/locales/es-ES.json index 6624a6a9..041b1329 100644 --- a/src/workspaces/officerdev/src/locales/es-ES.json +++ b/src/workspaces/officerdev/src/locales/es-ES.json @@ -257,7 +257,11 @@ }, "startHint": "Mirará aquello que le has señalado y luego te preguntará qué debe vigilar.", "startBlocked": "{{missing}} — y entonces esto lo inicia.", - "confirmDeletingNamed": "Confirmar la eliminación de {{name}}" + "confirmDeletingNamed": "Confirmar la eliminación de {{name}}", + "fromPlugins": "De plugins", + "available": "Disponible", + "configureAndCreate": "Configurar y crear", + "notCreatedYet": "Aún no creado — configúralo para empezar." }, "chatHistory": { "newChat": "Nuevo chat", diff --git a/src/workspaces/officerdev/src/locales/fr-FR.json b/src/workspaces/officerdev/src/locales/fr-FR.json index 4757a860..a49c2d45 100644 --- a/src/workspaces/officerdev/src/locales/fr-FR.json +++ b/src/workspaces/officerdev/src/locales/fr-FR.json @@ -257,7 +257,11 @@ }, "startHint": "Il va regarder ce vers quoi vous l'avez orienté, puis vous demander ce qu'il doit surveiller.", "startBlocked": "{{missing}} — ensuite ceci le démarre.", - "confirmDeletingNamed": "Confirmer la suppression de {{name}}" + "confirmDeletingNamed": "Confirmer la suppression de {{name}}", + "fromPlugins": "Depuis les plugins", + "available": "Disponible", + "configureAndCreate": "Configurer et créer", + "notCreatedYet": "Pas encore créé — configurez-le pour commencer." }, "chatHistory": { "newChat": "Nouveau Chat", diff --git a/src/workspaces/officerdev/src/locales/pt-PT.json b/src/workspaces/officerdev/src/locales/pt-PT.json index 4909e9c2..ef521468 100644 --- a/src/workspaces/officerdev/src/locales/pt-PT.json +++ b/src/workspaces/officerdev/src/locales/pt-PT.json @@ -257,7 +257,11 @@ }, "startHint": "Vai olhar para aquilo que lhe indicou e depois perguntar-lhe o que deve vigiar.", "startBlocked": "{{missing}} — depois é isto que o inicia.", - "confirmDeletingNamed": "Confirmar a eliminação de {{name}}" + "confirmDeletingNamed": "Confirmar a eliminação de {{name}}", + "fromPlugins": "De plugins", + "available": "Disponível", + "configureAndCreate": "Configurar e criar", + "notCreatedYet": "Ainda não criado — configure para começar." }, "chatHistory": { "newChat": "Novo chat", diff --git a/src/workspaces/officerdev/src/locales/sv-SE.json b/src/workspaces/officerdev/src/locales/sv-SE.json index 2322aff0..3a261f01 100644 --- a/src/workspaces/officerdev/src/locales/sv-SE.json +++ b/src/workspaces/officerdev/src/locales/sv-SE.json @@ -257,7 +257,11 @@ }, "startHint": "Den tittar på det du pekade den mot, och frågar dig sedan vad den ska bevaka.", "startBlocked": "{{missing}} — sedan startar detta den.", - "confirmDeletingNamed": "Bekräfta borttagning av {{name}}" + "confirmDeletingNamed": "Bekräfta borttagning av {{name}}", + "fromPlugins": "Från plugin", + "available": "Tillgänglig", + "configureAndCreate": "Konfigurera och skapa", + "notCreatedYet": "Inte skapad än — konfigurera för att starta." }, "chatHistory": { "newChat": "Ny chatt",