diff --git a/src/components/island/Island.svelte b/src/components/island/Island.svelte
index 429189b..3bd6bad 100644
--- a/src/components/island/Island.svelte
+++ b/src/components/island/Island.svelte
@@ -53,6 +53,7 @@
import { FinishWatch } from '../../lib/island/finished.svelte';
import { posed } from '../../lib/island/posed.svelte';
import { PrWatch } from '../../lib/island/pr-watch.svelte';
+ import { alertPlan } from '../../lib/island/pressure';
import { MemoryWatch } from '../../lib/island/pressure.svelte';
import { QuotaWatch } from '../../lib/island/quotas.svelte';
import { RecapReader } from '../../lib/island/recap.svelte';
@@ -155,8 +156,10 @@
if (!client || demo) return;
return untrack(() => update.follow(client));
});
+ /** Where Settings lets the warning show: by default the pill, the island left closed (`alertPlan`). */
+ const memoryPlan = $derived(alertPlan(companion.prefs.memoryAlert));
/** The warning's cause, not its numbers: a warning calls the island once, not on every read. */
- const memoryCause = $derived(memory.warning ? `${memory.warning.reason}:${memory.warning.threadId ?? ''}` : null);
+ const memoryCause = $derived(memory.warning && memoryPlan.opens ? `${memory.warning.reason}:${memory.warning.threadId ?? ''}` : null);
/** The day's finished threads, read for the tabs that show them, as they show. */
const recap = new RecapReader({ client: () => companion.client, projects: () => companion.projects, leftOut: () => companion.ownThreads });
/** The room between a drop and the island. */
@@ -305,7 +308,7 @@
return untrack(() => frame.call('alarm'));
});
- // The memory guard comes near: the island opens on its card, amber, until it is stopped or set aside.
+ // The memory guard comes near and Settings asks for the panel: the island opens on its card, amber, until it is stopped or set aside.
$effect(() => {
if (!memoryCause) return;
untrack(() => companion.cue('call'));
@@ -318,7 +321,7 @@
* requests come first, and nothing shows over a full-screen app or during
* focus: the card waits for them.
*/
- const away = new AwayWatch({ rows: () => sessions.rows, pr: () => prs.told, warning: () => memory.warning, stopped: () => memory.stopped });
+ const away = new AwayWatch({ rows: () => sessions.rows, pr: () => prs.told, warning: () => (memoryPlan.card ? memory.warning : null), stopped: () => (memoryPlan.card ? memory.stopped : null) });
$effect(() => away.start());
const awayQuotas = new QuotaWatch(() => ({ shown: companion.prefs.quotas, hidden: companion.prefs.hiddenQuotas }));
/** A return older than this was told before the island was drawn again. */
@@ -339,7 +342,8 @@
waiting: pose?.waiting ?? waitingOnUser(input, new Map(), now, 0, false).map(({ threadId, title, kind, since }) => ({ threadId, title, kind, since })),
alarms: pose?.alarms ?? companion.notes.alarms,
quota: pose ? pose.quota : tight && { name: tight.account.name, left: tight.bar.left },
- seen: away.seen
+ seen: away.seen,
+ memoryOff: !memoryPlan.card
};
}
@@ -453,7 +457,7 @@
ondrop={drop}
>
{#if frame.size !== 'open'}
-
+
{:else}
@@ -88,6 +118,55 @@
.memory.red {
--tone: var(--island-danger);
}
+ /* The default: one slim line, the details a press away. */
+ .memory.strip {
+ align-items: center;
+ gap: 6px;
+ padding: 4px 6px 4px 4px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--tone) 8%, var(--island-card));
+ }
+ .open {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ height: 24px;
+ padding: 0 6px 0 0;
+ border: none;
+ border-radius: 999px;
+ background: none;
+ color: var(--island-text);
+ font: inherit;
+ font-size: 12px;
+ text-align: left;
+ cursor: pointer;
+ }
+ .open :global(svg:last-child) {
+ flex: none;
+ color: var(--island-muted);
+ }
+ .open:hover :global(svg:last-child) {
+ color: var(--island-text);
+ }
+ .said-line {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ font-variant-numeric: tabular-nums;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .icon.small {
+ width: 22px;
+ height: 22px;
+ }
+ .button.slim {
+ flex: none;
+ height: 24px;
+ padding: 0 10px;
+ }
.memory.quiet {
--tone: var(--island-muted);
align-items: center;
@@ -206,6 +285,7 @@
color: var(--island-text);
}
.button:focus-visible,
+ .open:focus-visible,
.close:focus-visible {
outline: 2px solid var(--island-talking);
outline-offset: 1px;
diff --git a/src/components/island/views/memory-card.svelte.test.ts b/src/components/island/views/memory-card.svelte.test.ts
new file mode 100644
index 0000000..fc9ca44
--- /dev/null
+++ b/src/components/island/views/memory-card.svelte.test.ts
@@ -0,0 +1,52 @@
+/*
+ * The memory warning over the deck. By default it is a slim strip, the full
+ * card a press away; Settings can ask for the full card at once, or nothing.
+ */
+import { afterEach, expect, test } from 'vitest';
+import { flushSync, mount, unmount } from 'svelte';
+import type { Companion } from '../../../lib/companion/companion.svelte';
+import { DEFAULT_COMPANION_PREFS, type MemoryAlert } from '../../../lib/companion/prefs';
+import { viewContext, type ViewContext } from '../../../lib/island/contract';
+import { MemoryWatch } from '../../../lib/island/pressure.svelte';
+import MemoryCard from './MemoryCard.svelte';
+
+const GB = 1024 * 1024 * 1024;
+
+let shown: { card: Record; target: HTMLElement } | null = null;
+
+function show(memoryAlert: MemoryAlert = DEFAULT_COMPANION_PREFS.memoryAlert): HTMLElement {
+ const memory = new MemoryWatch();
+ memory.pose({ reason: 'machine', threadId: 't-1', title: 'Refonte dashboard', bytes: 0.6 * GB, used: 2.8 * GB, limit: 3.2 * GB });
+ const target = document.body.appendChild(document.createElement('div'));
+ const context = { companion: { prefs: { ...DEFAULT_COMPANION_PREFS, memoryAlert } } as unknown as Companion, core: { client: null, reachable: true }, memory } as unknown as ViewContext;
+ shown = { card: mount(MemoryCard, { target, context: viewContext(context) }), target };
+ flushSync();
+ return target;
+}
+
+function hide() {
+ if (!shown) return;
+ unmount(shown.card);
+ shown.target.remove();
+ shown = null;
+}
+
+afterEach(hide);
+
+const testid = (target: HTMLElement, id: string) => target.querySelector(`[data-testid="${id}"]`);
+
+test('by default the warning is a slim strip, not the full card, and opens on a press', () => {
+ const target = show();
+ expect(testid(target, 'island-memory')).toBeNull();
+ expect(testid(target, 'island-memory-strip')?.textContent).toContain('2.8 GB');
+ (testid(target, 'memory-details') as HTMLButtonElement).click();
+ flushSync();
+ expect(testid(target, 'island-memory')).not.toBeNull();
+ expect(testid(target, 'island-memory-strip')).toBeNull();
+});
+
+test('the panel setting shows the full card at once, off shows nothing', () => {
+ expect(testid(show('panel'), 'island-memory')).not.toBeNull();
+ hide();
+ expect(show('off').querySelector('section')).toBeNull();
+});
diff --git a/src/lib/companion/companion.test.ts b/src/lib/companion/companion.test.ts
index 30a3cce..3058722 100644
--- a/src/lib/companion/companion.test.ts
+++ b/src/lib/companion/companion.test.ts
@@ -30,6 +30,9 @@ test('stored preferences are read field by field, a wrong field taking its defau
expect(parseCompanionPrefs({ pillRest: 'timer' }).pillRest).toBe('timer');
expect(parseCompanionPrefs({}).awayRecap).toBe(true);
expect(parseCompanionPrefs({ awayRecap: false }).awayRecap).toBe(false);
+ expect(parseCompanionPrefs({}).memoryAlert).toBe('pill');
+ expect(parseCompanionPrefs({ memoryAlert: 'off' }).memoryAlert).toBe('off');
+ expect(parseCompanionPrefs({ memoryAlert: 'loud' }).memoryAlert).toBe('pill');
window.localStorage.setItem(COMPANION_STORAGE_KEY, '{not json');
expect(readCompanionPrefs()).toEqual(DEFAULT_COMPANION_PREFS);
});
diff --git a/src/lib/companion/prefs.ts b/src/lib/companion/prefs.ts
index 98cddf0..3da879c 100644
--- a/src/lib/companion/prefs.ts
+++ b/src/lib/companion/prefs.ts
@@ -27,6 +27,9 @@ export const PILL_RESTS = ['live', 'quota', 'music', 'timer', 'robot'] as const
export type IslandPlace = `${IslandEdge}-${IslandSide}`;
export type IslandEdge = 'top' | 'bottom';
export type IslandSide = 'left' | 'center' | 'right';
+/** How the memory guard's warning shows: a line in the pill and a slim one over the deck, the deck opened on its full card, or not at all. */
+export type MemoryAlert = 'pill' | 'panel' | 'off';
+export const MEMORY_ALERTS = ['pill', 'panel', 'off'] as const satisfies readonly MemoryAlert[];
export const ISLAND_PLACES = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'] as const satisfies readonly IslandPlace[];
/** The character's centre, in fractions of the screen's work area. */
export interface CompanionSpot { x: number; y: number }
@@ -84,6 +87,7 @@ export interface CompanionPrefs {
radarMinutes: number;
/** Back at the keyboard after a long absence, the island tells what happened meanwhile (`island/away.ts`). */
awayRecap: boolean;
+ memoryAlert: MemoryAlert;
screenScope: CompanionScreenScope;
/** The agents that stand as the companion, by id, the leader first; `CREW_MAX` at most. */
agents: string[];
@@ -122,6 +126,7 @@ export const DEFAULT_COMPANION_PREFS: CompanionPrefs = {
focusOnWork: true,
radarMinutes: 10,
awayRecap: true,
+ memoryAlert: 'pill',
screenScope: 'all',
agents: [],
crewMade: false,
@@ -189,6 +194,7 @@ export function parseCompanionPrefs(raw: unknown): CompanionPrefs {
focusOnWork: value.focusOnWork !== false,
radarMinutes: value.radarMinutes === 0 ? 0 : minutes(value.radarMinutes, 240, DEFAULT_COMPANION_PREFS.radarMinutes),
awayRecap: value.awayRecap !== false,
+ memoryAlert: (MEMORY_ALERTS as readonly unknown[]).includes(value.memoryAlert) ? (value.memoryAlert as MemoryAlert) : 'pill',
screenScope: value.screenScope === 'here' || value.screenScope === 'zone' ? value.screenScope : 'all',
agents: Array.isArray(value.agents) ? [...new Set(value.agents.filter((id): id is string => typeof id === 'string' && id !== ''))].slice(0, CREW_MAX) : [],
crewMade: value.crewMade === true,
diff --git a/src/lib/island/away.test.ts b/src/lib/island/away.test.ts
index e570459..2b24322 100644
--- a/src/lib/island/away.test.ts
+++ b/src/lib/island/away.test.ts
@@ -125,6 +125,8 @@ test('the memory guard: what it stopped, or the machine running low, the latest
{ key: 'memory', icon: 'memory', text: fill(copy.memoryStopped, { program: 'node.exe', title: 'Build' }), tone: 'failed', target: { kind: 'thread', threadId: 't' } }
]);
expect(awayLines(facts({ events: [machine] }))[0]).toMatchObject({ text: copy.memoryMachine, target: null });
+ // The alert turned off once the card shows: what was logged before goes too.
+ expect(awayLines(facts({ events: [machine, stopped], memoryOff: true }))).toEqual([]);
});
test('only the last day is kept', () => {
diff --git a/src/lib/island/away.ts b/src/lib/island/away.ts
index 8ddf973..f811d82 100644
--- a/src/lib/island/away.ts
+++ b/src/lib/island/away.ts
@@ -69,6 +69,8 @@ export interface AwayFacts {
quota: AwayQuota | null;
/** The lines already opened from the card. */
seen?: readonly string[];
+ /** Settings turned the memory alert off: the guard's events are not told, those logged before included. */
+ memoryOff?: boolean;
}
/** What a line opens: a thread in Boite, or the island's Home. */
@@ -191,7 +193,7 @@ export function awayLines(facts: AwayFacts): AwayLine[] {
const out = left <= 0;
lines.push({ key: 'quota', icon: 'quota', text: out ? fill(copy.quotaOut, { name }) : fill(copy.quotaLow, { name, left: String(Math.round(left)) }), tone: out ? 'failed' : 'attention', target: null });
}
- const memory = events.filter((event): event is MemoryEvent => event.kind === 'memory').at(-1);
+ const memory = facts.memoryOff ? undefined : events.filter((event): event is MemoryEvent => event.kind === 'memory').at(-1);
if (memory) lines.push(memoryLine(memory));
const seen = new Set(facts.seen ?? []);
return lines.filter((line) => !seen.has(line.key));
diff --git a/src/lib/island/pill-view.test.ts b/src/lib/island/pill-view.test.ts
index e09807b..72804bd 100644
--- a/src/lib/island/pill-view.test.ts
+++ b/src/lib/island/pill-view.test.ts
@@ -1,10 +1,11 @@
import { expect, test } from 'vitest';
import { fill, strings } from '../strings';
import type { RestLine } from './pill-rest';
+import type { Pressure } from './pressure';
import { emphasis, fitWords, pillText, pillView, TITLE_MAX, type PillFacts } from './pill-view';
const copy = strings.island.pill;
-const calm: PillFacts = { waiting: { count: 0, from: null }, told: null, rule: null, rest: null, robot: false, focus: null, working: { count: 0, first: null }, goal: null };
+const calm: PillFacts = { waiting: { count: 0, from: null }, told: null, rule: null, memory: null, rest: null, robot: false, focus: null, working: { count: 0, first: null }, goal: null };
const busy: PillFacts = { ...calm, working: { count: 3, first: 'Bench against legacy' }, goal: { done: 2, total: 5 } };
const quota: RestLine = { name: 'Max', doing: '18 % left', progress: null, kind: 'quota', tone: 'amber' };
@@ -72,3 +73,17 @@ test('a template is split around its value', () => {
expect(emphasis('{name} is waiting for you', 'name', 'Atlas')).toEqual({ before: '', strong: 'Atlas', after: ' is waiting for you' });
expect(emphasis('Wait for {name}', 'name', 'Atlas')).toEqual({ before: 'Wait for ', strong: 'Atlas', after: '' });
});
+
+test('the memory warning comes after a request and a thing told, before the rest and the agents, its chip tinted', () => {
+ const GB = 1024 * 1024 * 1024;
+ const pressure: Pressure = { reason: 'machine', threadId: 't', title: 'Unity build', bytes: 0.6 * GB, used: 2.8 * GB, limit: 3.2 * GB };
+ const memory = { pressure, red: true };
+ expect(pillView({ ...busy, waiting: { count: 1, from: 'Atlas' }, memory }).state).toBe('waiting');
+ expect(pillView({ ...busy, told: { text: 'Port the scheduler · done', tone: 'done' }, memory }).state).toBe('told');
+ const view = pillView({ ...busy, rest: quota, robot: true, memory });
+ expect(view).toMatchObject({ state: 'rest', tone: 'plain', glyph: 'memory', glyphTone: 'red', strong: '2.8 GB', meta: null });
+ expect(pillText(view)).toBe(fill(copy.memory.machine, { used: '2.8 GB' }));
+ const thread = pillView({ ...calm, memory: { pressure: { ...pressure, reason: 'thread', used: 3.5 * GB, limit: 4 * GB }, red: false } });
+ expect(pillText(thread)).toBe(fill(copy.memory.thread, { title: 'Unity build', used: '3.5 GB', limit: '4.0 GB' }));
+ expect(thread.glyphTone).toBe('amber');
+});
diff --git a/src/lib/island/pill-view.ts b/src/lib/island/pill-view.ts
index e74e48d..09181c4 100644
--- a/src/lib/island/pill-view.ts
+++ b/src/lib/island/pill-view.ts
@@ -5,11 +5,13 @@
* that asks ("Atlas attend votre réponse"), or how many wait.
* 2. Something was just told: a thread ended, a pull request's CI settled
* (green, red when it failed), or a rule answered a request (its shield).
- * 3. What Settings shows at rest instead of the agents: the quota, the music,
+ * 3. The memory guard's warning, when Settings keeps it in the pill (its
+ * default): its chip tinted amber or red, the number that matters bold.
+ * 4. What Settings shows at rest instead of the agents: the quota, the music,
* the timer (`pill-rest.ts`), or the robot alone.
- * 4. Agents at work: how many ("3 au travail"), or the one followed from Home
+ * 5. Agents at work: how many ("3 au travail"), or the one followed from Home
* or picked with the wheel, with a task list's progress when one has it.
- * 5. Nothing: the robot alone, small and quiet.
+ * 6. Nothing: the robot alone, small and quiet.
*
* The words never stop mid-word: long titles are cut on a word (`fitWords`).
* The per-thread detail (each thread's state, its step) is the hover's
@@ -17,12 +19,13 @@
*/
import { fill, strings } from '../strings';
import type { RestLine } from './pill-rest';
+import { sizeText, type Pressure } from './pressure';
export type PillState = 'waiting' | 'told' | 'rest' | 'working' | 'idle';
/** The words' colour: amber only while someone waits on the user. */
export type PillTone = 'attention' | 'done' | 'failed' | 'plain';
/** The mark before the words, tinted on its own (`glyphTone`). */
-export type PillGlyph = 'rule' | 'quota' | 'music' | 'timer' | null;
+export type PillGlyph = 'rule' | 'quota' | 'music' | 'timer' | 'memory' | null;
export interface PillView {
state: PillState;
@@ -49,6 +52,8 @@ export interface PillFacts {
told: { text: string; tone: 'done' | 'failed' } | null;
/** What a rule just allowed, said in full. */
rule: string | null;
+ /** The memory guard's warning, shown in the pill, red when it is close. */
+ memory: { pressure: Pressure; red: boolean } | null;
/** The quota, the music or the timer, when Settings shows one at rest and it has something to show. */
rest: RestLine | null;
/** Settings shows the robot alone at rest. */
@@ -109,6 +114,12 @@ export function pillView(facts: PillFacts): PillView {
}
if (facts.told) return { ...QUIET, state: 'told', tone: facts.told.tone, after: fitWords(facts.told.text, TOLD_MAX) };
if (facts.rule) return { ...QUIET, state: 'told', tone: 'done', glyph: 'rule', after: fitWords(facts.rule, TOLD_MAX) };
+ if (facts.memory) {
+ const { pressure, red } = facts.memory;
+ const units = strings.island.memory;
+ const template = fill(copy.memory[pressure.reason], { title: fitWords(pressure.title || units.agent, TITLE_MAX), limit: sizeText(pressure.limit, units) });
+ return { ...QUIET, state: 'rest', glyph: 'memory', glyphTone: red ? 'red' : 'amber', ...emphasis(template, 'used', sizeText(pressure.used, units)) };
+ }
if (facts.robot) return QUIET;
if (facts.rest) {
const line = facts.rest;
diff --git a/src/lib/island/pressure.test.ts b/src/lib/island/pressure.test.ts
index ce9c9a2..c375972 100644
--- a/src/lib/island/pressure.test.ts
+++ b/src/lib/island/pressure.test.ts
@@ -1,6 +1,7 @@
import { expect, test } from 'vitest';
import type { MemoryEvent, MemoryStatus } from '@boite/contracts';
-import { CALM_SHARE, guardOff, heaviest, memoryGauge, NEAR_SHARE, nearness, pressureOf, samePressure, sizeText, stoppedOf, type AgentLoad } from './pressure';
+import { DEFAULT_COMPANION_PREFS } from '../companion/prefs';
+import { alertPlan, CALM_SHARE, guardOff, heaviest, memoryGauge, NEAR_SHARE, nearness, pressureOf, pressureRed, samePressure, sizeText, stoppedOf, type AgentLoad } from './pressure';
const MB = 1024 * 1024;
const GB = 1024 * MB;
@@ -89,4 +90,16 @@ test("the deck's gauge reads the agents against their budget, and turns as the w
expect(memoryGauge(status({ agentBytes: 12 * GB })).share).toBe(1);
const off = memoryGauge(status({ agentBytes: 3 * GB }, { budgetMb: 0, threadMemoryCapMb: 0, memoryReserveMb: 0 }));
expect([off.budget, off.share, off.tone]).toEqual([null, null, 'ok']);
-});
\ No newline at end of file
+});
+test('by default a warning stays in the pill and a slim strip: the island neither opens nor chimes', () => {
+ expect(alertPlan(DEFAULT_COMPANION_PREFS.memoryAlert)).toEqual({ pill: true, card: 'line', opens: false });
+ expect(alertPlan('panel')).toEqual({ pill: false, card: 'full', opens: true });
+ expect(alertPlan('off')).toEqual({ pill: false, card: null, opens: false });
+});
+
+test('a warning is red when the machine is short or a limit is all but reached', () => {
+ const near = { reason: 'thread' as const, threadId: 'a', title: '', bytes: 0, used: 3.5 * GB, limit: 4 * GB };
+ expect(pressureRed(near)).toBe(false);
+ expect(pressureRed({ ...near, used: 3.9 * GB })).toBe(true);
+ expect(pressureRed({ ...near, reason: 'machine' })).toBe(true);
+});
diff --git a/src/lib/island/pressure.ts b/src/lib/island/pressure.ts
index ef8ef9a..264b862 100644
--- a/src/lib/island/pressure.ts
+++ b/src/lib/island/pressure.ts
@@ -9,6 +9,7 @@
* under the reserve. The island warns a step before each.
*/
import type { AgentResourceUsage, MemoryEvent, MemoryStatus } from '@boite/contracts';
+import type { MemoryAlert } from '../companion/prefs';
/** The share of a cap or a budget past which the island warns. */
export const WARN_SHARE = 0.85;
@@ -113,6 +114,38 @@ export function samePressure(a: Pressure | null, b: Pressure | null): boolean {
return a === b || (a !== null && b !== null && a.reason === b.reason && a.threadId === b.threadId);
}
+/** Past this share of the limit, a warning turns red. */
+export const RED_SHARE = 0.95;
+
+/** How full the measured limit is, for the card's bar; null for the free memory, which has none. */
+export function pressureShare(pressure: Pressure): number | null {
+ return pressure.reason !== 'machine' && pressure.limit > 0 ? Math.min(1, pressure.used / pressure.limit) : null;
+}
+
+/** Red: the machine itself is short, or a limit is all but reached. */
+export function pressureRed(pressure: Pressure): boolean {
+ return pressure.reason === 'machine' || (pressureShare(pressure) ?? 0) >= RED_SHARE;
+}
+
+/**
+ * Where a warning shows, by the user's choice (`prefs.memoryAlert`): by
+ * default a line in the pill and a slim one over the deck, the island left
+ * closed and quiet; `panel` opens the deck on the full card with a chime;
+ * `off` shows nothing, what the guard stopped included.
+ */
+export interface AlertPlan {
+ pill: boolean;
+ card: 'line' | 'full' | null;
+ /** The island opens on the card and chimes. */
+ opens: boolean;
+}
+
+export function alertPlan(alert: MemoryAlert): AlertPlan {
+ if (alert === 'panel') return { pill: false, card: 'full', opens: true };
+ if (alert === 'off') return { pill: false, card: null, opens: false };
+ return { pill: true, card: 'line', opens: false };
+}
+
/** The deck's memory gauge: what the agents hold, the share of their budget (null without one), the machine's free memory. */
export interface MemoryGauge {
used: number;
diff --git a/src/lib/strings.en.ts b/src/lib/strings.en.ts
index b919e27..95e6dd1 100644
--- a/src/lib/strings.en.ts
+++ b/src/lib/strings.en.ts
@@ -60,7 +60,14 @@ export const strings = {
waitingMany: '{count} requests wait for you',
finished: 'Done',
/** The collapsed pill's one line (`pill-view.ts`): its `{name}` or `{count}` is set in bold. */
- pill: { waitsOne: '{name} is waiting for you', waitsMany: '{count} are waiting for you', someone: 'An agent', workingOne: '{name} is working', workingMany: '{count} working' },
+ pill: {
+ waitsOne: '{name} is waiting for you',
+ waitsMany: '{count} are waiting for you',
+ someone: 'An agent',
+ workingOne: '{name} is working',
+ workingMany: '{count} working',
+ memory: { thread: '{title}: {used} of {limit}', budget: 'Agents’ memory: {used} of {limit}', machine: 'Low memory: {used} free' }
+ },
/** A thread that just ended, told in the pill: "Port the scheduler · done · 3 files". */
finish: {
done: 'done',
@@ -241,6 +248,7 @@ export const strings = {
stop: 'Stop the agent',
stopping: 'Stopping…',
stopTitle: 'Stop this thread’s processes, as Boite’s task manager does',
+ details: 'Show details',
ignore: 'Ignore',
failed: 'The agent could not be stopped: {reason}',
stopped: 'Boite stopped {program} in “{title}” to free {size}',
@@ -755,6 +763,9 @@ export const strings = {
clipboardHistoryHint: 'The deck keeps your last 30 copies, text and images, in memory only: they are lost when B.O.T.S quits, and passwords marked private by their app are skipped. Off, nothing is recorded.',
awayRecap: 'Recap when you come back',
awayRecapHint: 'Back after 15 minutes or more away, the island tells what finished, what waits for you and what rang meanwhile.',
+ memoryAlert: 'Memory alert',
+ memoryAlertHint: 'When Boite nears its memory limits. Quiet: a line in the island, which stays closed, and a slim strip at the top of the deck. Panel: the island opens on the full card, with a chime.',
+ memoryAlerts: { pill: 'Quiet', panel: 'Panel', off: 'Off' },
radar: 'Waiting threads',
radarHint: 'When threads have waited for you this long (a permission, a question, an answer you have not read), B.O.T.S says so and opens the first on a click.',
radarOff: 'Off',
diff --git a/src/lib/strings.fr.ts b/src/lib/strings.fr.ts
index 60ddaf2..de37b9d 100644
--- a/src/lib/strings.fr.ts
+++ b/src/lib/strings.fr.ts
@@ -49,7 +49,14 @@ export const fr: Messages = {
waitingOne: 'Un agent a besoin de vous',
waitingMany: '{count} demandes vous attendent',
finished: 'Terminé',
- pill: { waitsOne: '{name} attend votre réponse', waitsMany: '{count} vous attendent', someone: 'Un agent', workingOne: '{name} au travail', workingMany: '{count} au travail' },
+ pill: {
+ waitsOne: '{name} attend votre réponse',
+ waitsMany: '{count} vous attendent',
+ someone: 'Un agent',
+ workingOne: '{name} au travail',
+ workingMany: '{count} au travail',
+ memory: { thread: '{title} : {used} sur {limit}', budget: 'Mémoire des agents : {used} sur {limit}', machine: 'Mémoire basse : {used} libres' }
+ },
finish: {
done: 'terminé',
failed: 'en échec',
@@ -218,6 +225,7 @@ export const fr: Messages = {
stop: 'Arrêter l’agent',
stopping: 'Arrêt…',
stopTitle: 'Arrêter les processus de ce fil, comme le gestionnaire des tâches de Boite',
+ details: 'Voir le détail',
ignore: 'Ignorer',
failed: 'L’agent n’a pas pu être arrêté : {reason}',
stopped: 'Boite a arrêté {program} dans « {title} » pour libérer {size}',
@@ -729,6 +737,9 @@ export const fr: Messages = {
clipboardHistoryHint: 'Le deck garde vos 30 dernières copies, textes et images, en mémoire seulement : elles disparaissent quand B.O.T.S se ferme, et les mots de passe que leur appli marque comme privés sont ignorés. Désactivé, rien n’est enregistré.',
awayRecap: 'Récapitulatif à votre retour',
awayRecapHint: 'De retour après 15 minutes d’absence ou plus, l’île vous dit ce qui s’est terminé, ce qui vous attend et ce qui a sonné entre-temps.',
+ memoryAlert: 'Alerte mémoire',
+ memoryAlertHint: 'Quand Boite approche de ses limites de mémoire. Discrète : une ligne dans l’île, qui reste fermée, et une bande fine en haut du panneau. Panneau : l’île s’ouvre sur la carte complète, avec un son.',
+ memoryAlerts: { pill: 'Discrète', panel: 'Panneau', off: 'Désactivée' },
radar: 'Fils en attente',
radarHint: 'Quand des fils vous attendent depuis ce temps (une autorisation, une question, une réponse non lue), B.O.T.S vous le dit et ouvre le premier d’un clic.',
radarOff: 'Désactivé',