From b465f94b1ca3c83ea94d0bb8fd33ac6b84708caa Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:00 +0530 Subject: [PATCH 1/9] feat(orchestration): add latest_message_preview store query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newest message body for a session (by timestamp then seq), scoped to (agent, session) — powers the roster's one-line current-task, following the same per-session read pattern as unread_count. --- src/openhuman/orchestration/store.rs | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/openhuman/orchestration/store.rs b/src/openhuman/orchestration/store.rs index 22a40f1ea2..394574f1b0 100644 --- a/src/openhuman/orchestration/store.rs +++ b/src/openhuman/orchestration/store.rs @@ -230,6 +230,26 @@ pub fn session_last_seq( .optional()?) } +/// The most recent message body for a session — the roster task line's "current +/// activity". Newest by timestamp then seq; `None` when the session has no +/// messages yet. Body is decrypted plaintext (workspace-internal, like the rest +/// of this store). +pub fn latest_message_preview( + conn: &Connection, + agent_id: &str, + session_id: &str, +) -> Result> { + Ok(conn + .query_row( + "SELECT body FROM messages + WHERE agent_id = ?1 AND session_id = ?2 + ORDER BY timestamp DESC, seq DESC LIMIT 1", + params![agent_id, session_id], + |row| row.get(0), + ) + .optional()?) +} + /// List every persisted session row, newest activity first (stage-7 read surface). pub fn list_sessions(conn: &Connection) -> Result> { let mut stmt = conn.prepare( @@ -748,6 +768,31 @@ mod tests { .unwrap(); } + #[test] + fn latest_message_preview_returns_newest_or_none() { + let tmp = tempfile::tempdir().unwrap(); + with_connection(tmp.path(), |conn| { + upsert_session(conn, &session("@a", "h1", 1))?; + // No messages yet. + assert_eq!(latest_message_preview(conn, "@a", "h1")?, None); + + // Same timestamp → newest is decided by seq DESC. + insert_message(conn, &msg("m1", "@a", "h1", 1))?; + let mut newer = msg("m2", "@a", "h1", 2); + newer.body = "later line".into(); + insert_message(conn, &newer)?; + assert_eq!( + latest_message_preview(conn, "@a", "h1")?.as_deref(), + Some("later line") + ); + + // Scoped to (agent, session): a different session is not returned. + assert_eq!(latest_message_preview(conn, "@a", "other")?, None); + Ok(()) + }) + .unwrap(); + } + #[test] fn world_diff_is_append_only_with_monotonic_seq_and_idempotent_cycles() { let tmp = tempfile::tempdir().unwrap(); From 4bccfb741ea20655329c6b9db6d99338965a2803 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:01 +0530 Subject: [PATCH 2/9] feat(orchestration): expose harnessType/status/currentTask on session summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionSummary now carries the emitting harness (derived from the session source: claude/codex/gemini), a coarse status (idle when active, else stopped — richer run-state deferred), and the latest-message task preview (UTF-8-safe truncation). Pinned/sentinel windows report no harness. Unit-tested. --- src/openhuman/orchestration/schemas.rs | 125 ++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/src/openhuman/orchestration/schemas.rs b/src/openhuman/orchestration/schemas.rs index 9321ca2ccd..f7a80135e9 100644 --- a/src/openhuman/orchestration/schemas.rs +++ b/src/openhuman/orchestration/schemas.rs @@ -132,6 +132,15 @@ struct SessionSummary { session_id: String, agent_id: String, source: String, + /// The emitting harness (claude/codex/gemini) when this is an external agent + /// instance; absent for the pinned master/subconscious/user-created windows. + #[serde(skip_serializing_if = "Option::is_none")] + harness_type: Option, + /// Coarse instance status for the roster status dot (see `derive_status`). + status: String, + /// One-line current activity (latest message preview) for the roster. + #[serde(skip_serializing_if = "Option::is_none")] + current_task: Option, #[serde(skip_serializing_if = "Option::is_none")] label: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -196,14 +205,60 @@ fn is_active(last_message_at: &str) -> bool { } } -fn summarize(session: OrchestrationSession, unread: i64, pinned: bool) -> SessionSummary { +/// The harness provider for a session, when its `source` names one. Session +/// windows persist the emitting harness (claude/codex/gemini) in `source` (see +/// `ingest.rs`); the sentinel windows (master/subconscious/user_created/ +/// orchestration) carry no harness and yield `None`. +fn harness_type_for(source: &str) -> Option { + matches!(source, "claude" | "codex" | "gemini").then(|| source.to_string()) +} + +/// Coarse instance status for the roster dot, derived from activity. Peer +/// instances carry no true run-state yet, so today an instance is `idle` when it +/// has recent traffic and `stopped` otherwise. The richer +/// running/waiting-approval/errored states are reserved for the attention-queue +/// and run-state follow-ups; the renderer's `InstanceStatusDot` already models +/// all five. +fn derive_status(active: bool) -> &'static str { + if active { + "idle" + } else { + "stopped" + } +} + +/// One-line, UTF-8-safe preview of a message body for the roster task line. +/// Truncates on a char boundary and reserves room for the ellipsis so the result +/// never exceeds `MAX` chars (avoids the byte-slice panics noted in the codebase). +fn task_preview(body: &str) -> String { + const MAX: usize = 80; + let trimmed = body.trim(); + if trimmed.chars().count() <= MAX { + return trimmed.to_string(); + } + let mut out: String = trimmed.chars().take(MAX - 1).collect(); + out.push('…'); + out +} + +fn summarize( + session: OrchestrationSession, + unread: i64, + pinned: bool, + current_task: Option, +) -> SessionSummary { let chat_kind = chat_kind_for_session(&session.session_id); let active = pinned || is_active(&session.last_message_at); + let harness_type = harness_type_for(&session.source); + let status = derive_status(active).to_string(); SessionSummary { chat_kind: chat_kind.as_str().to_string(), active, unread, pinned, + harness_type, + status, + current_task, session_id: session.session_id, agent_id: session.agent_id, source: session.source, @@ -231,7 +286,15 @@ fn handle_sessions_list(_params: Map) -> ControllerFuture { _ => {} } let pinned = matches!(session.session_id.as_str(), "master" | "subconscious"); - out.push(summarize(session, unread, pinned)); + // Roster task line: latest message preview for real instance + // windows; the pinned windows don't need one. + let current_task = if pinned { + None + } else { + store::latest_message_preview(conn, &session.agent_id, &session.session_id)? + .map(|body| task_preview(&body)) + }; + out.push(summarize(session, unread, pinned, current_task)); } // Ensure the pinned windows always exist even before any traffic. if !have_master { @@ -278,7 +341,7 @@ fn handle_sessions_create(params: Map) -> ControllerFuture { }) .map_err(|e| format!("sessions_create: {e}"))?; super::bus::notify_orchestration_message(&agent_id, &session_id, "session"); - to_json(serde_json::json!({ "session": summarize(session, 0, false) })) + to_json(serde_json::json!({ "session": summarize(session, 0, false, None) })) }) } @@ -287,6 +350,9 @@ fn pinned_placeholder(session_id: &str) -> SessionSummary { session_id: session_id.to_string(), agent_id: session_id.to_string(), source: "orchestration".to_string(), + harness_type: None, + status: derive_status(true).to_string(), + current_task: None, label: None, workspace: None, chat_kind: chat_kind_for_session(session_id).as_str().to_string(), @@ -649,4 +715,57 @@ mod tests { params.insert("chat".to_string(), Value::String(" ".to_string())); assert!(required_param(¶ms, "chat").is_err()); } + + #[test] + fn harness_type_only_for_known_providers() { + assert_eq!(harness_type_for("claude").as_deref(), Some("claude")); + assert_eq!(harness_type_for("codex").as_deref(), Some("codex")); + assert_eq!(harness_type_for("gemini").as_deref(), Some("gemini")); + // Sentinel / origin sources are not harnesses. + assert_eq!(harness_type_for("master"), None); + assert_eq!(harness_type_for("user_created"), None); + assert_eq!(harness_type_for("orchestration"), None); + } + + #[test] + fn status_is_idle_when_active_else_stopped() { + assert_eq!(derive_status(true), "idle"); + assert_eq!(derive_status(false), "stopped"); + } + + #[test] + fn task_preview_trims_and_caps_on_char_boundary() { + assert_eq!(task_preview(" hello "), "hello"); + // A multibyte string longer than the cap truncates with an ellipsis and + // never exceeds MAX chars (no mid-codepoint panic). + let long = "é".repeat(200); + let preview = task_preview(&long); + assert_eq!(preview.chars().count(), 80); + assert!(preview.ends_with('…')); + } + + #[test] + fn summarize_derives_harness_status_and_carries_task() { + let session = OrchestrationSession { + session_id: "w1".to_string(), + agent_id: "@peer".to_string(), + source: "claude".to_string(), + label: None, + workspace: None, + last_seq: 3, + created_at: "2020-01-01T00:00:00Z".to_string(), + // Stale timestamp → not active → stopped. + last_message_at: "2020-01-01T00:00:00Z".to_string(), + }; + let summary = summarize(session, 2, false, Some("drafting cards".to_string())); + assert_eq!(summary.harness_type.as_deref(), Some("claude")); + assert_eq!(summary.status, "stopped"); + assert_eq!(summary.current_task.as_deref(), Some("drafting cards")); + assert!(!summary.active); + + // A pinned window is always active → idle, and carries no harness/task. + let pinned = pinned_placeholder("master"); + assert_eq!(pinned.status, "idle"); + assert!(pinned.harness_type.is_none()); + } } From 883a71ef0dcceeb193f5e6533d66197730fe6d80 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:08 +0530 Subject: [PATCH 3/9] feat(orchestration): extend SessionSummary client type with instance fields Adds HarnessType, InstanceStatus and the harnessType/status/currentTask fields to the renderer SessionSummary, matching the core RPC shape. --- .../lib/orchestration/orchestrationClient.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/lib/orchestration/orchestrationClient.ts b/app/src/lib/orchestration/orchestrationClient.ts index 38ed4f7a29..b8c38df2d4 100644 --- a/app/src/lib/orchestration/orchestrationClient.ts +++ b/app/src/lib/orchestration/orchestrationClient.ts @@ -20,10 +20,27 @@ export { PaymentRequiredError }; export type OrchestrationChatKind = 'master' | 'subconscious' | 'session'; +/** External agent harness that emits a session (drives the roster grouping). */ +export type HarnessType = 'claude' | 'codex' | 'gemini'; + +/** + * Coarse instance status for the roster dot. Peer instances carry no true + * run-state yet, so the core derives only `idle` / `stopped` today; the + * remaining states are modelled here (and by `InstanceStatusDot`) for the + * attention-queue and run-state follow-ups. + */ +export type InstanceStatus = 'running' | 'idle' | 'waiting-approval' | 'errored' | 'stopped'; + export interface SessionSummary { sessionId: string; agentId: string; source: string; + /** Emitting harness when this is an external instance; absent for master/subconscious/user-created. */ + harnessType?: HarnessType; + /** Coarse status for the roster dot (see {@link InstanceStatus}). */ + status: InstanceStatus; + /** One-line current activity (latest message preview) for the roster. */ + currentTask?: string; label?: string; workspace?: string; chatKind: OrchestrationChatKind; From b33dd78d7699b71c85e03cd89b3ad5f41ad47900 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:08 +0530 Subject: [PATCH 4/9] feat(intelligence): add InstanceStatusDot primitive Five-state status dot (running/idle/waiting-approval/errored/stopped) with color + pulse, the core roster primitive. Presentational, accessible label. --- .../intelligence/InstanceStatusDot.test.tsx | 27 ++++++++++++ .../intelligence/InstanceStatusDot.tsx | 42 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 app/src/components/intelligence/InstanceStatusDot.test.tsx create mode 100644 app/src/components/intelligence/InstanceStatusDot.tsx diff --git a/app/src/components/intelligence/InstanceStatusDot.test.tsx b/app/src/components/intelligence/InstanceStatusDot.test.tsx new file mode 100644 index 0000000000..3e6989933e --- /dev/null +++ b/app/src/components/intelligence/InstanceStatusDot.test.tsx @@ -0,0 +1,27 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import type { InstanceStatus } from '../../lib/orchestration/orchestrationClient'; +import InstanceStatusDot from './InstanceStatusDot'; + +describe('InstanceStatusDot', () => { + it.each(['running', 'idle', 'waiting-approval', 'errored', 'stopped'])( + 'tags the %s status and pulses only when running', + status => { + render(); + const dot = screen.getByTestId('instance-status-dot'); + expect(dot).toHaveAttribute('data-status', status); + expect(dot.className.includes('animate-pulse')).toBe(status === 'running'); + } + ); + + it('uses the status value as the accessible name when no label is given', () => { + render(); + expect(screen.getByTestId('instance-status-dot')).toHaveAttribute('aria-label', 'errored'); + }); + + it('prefers a provided translated label', () => { + render(); + expect(screen.getByTestId('instance-status-dot')).toHaveAttribute('aria-label', 'Inactivo'); + }); +}); diff --git a/app/src/components/intelligence/InstanceStatusDot.tsx b/app/src/components/intelligence/InstanceStatusDot.tsx new file mode 100644 index 0000000000..14f806e314 --- /dev/null +++ b/app/src/components/intelligence/InstanceStatusDot.tsx @@ -0,0 +1,42 @@ +/** + * InstanceStatusDot — the core roster primitive: one colored dot that encodes an + * agent instance's status at a glance, scannable across many rows. + * + * Five states (color + motion), matching the core's {@link InstanceStatus}: + * running (ocean, pulsing) · idle (sage) · waiting-approval (amber) · + * errored (coral) · stopped (faint). The core derives only idle/stopped today; + * the other three are wired ahead of the attention-queue / run-state work. + * + * Presentational only. Pass a translated `label` for the accessible name. + */ +import type { InstanceStatus } from '../../lib/orchestration/orchestrationClient'; + +export interface InstanceStatusDotProps { + status: InstanceStatus; + /** Accessible label (already translated by the caller). */ + label?: string; +} + +const TONE: Record = { + running: 'bg-ocean-500 animate-pulse', + idle: 'bg-sage-500', + 'waiting-approval': 'bg-amber-500', + errored: 'bg-coral-500', + stopped: 'bg-content-faint', +}; + +export default function InstanceStatusDot({ + status, + label, +}: InstanceStatusDotProps): React.ReactElement { + return ( + + ); +} From b5d05bfc9ee9f9cc18efb9aa815c4d75bd7339ec Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:09 +0530 Subject: [PATCH 5/9] feat(intelligence): add HarnessGlyph Brand mark for the driving harness (Claude/Codex/Gemini) plus an OpenHuman mark for internal windows; identity hues, token-based for the codex mark. --- .../intelligence/HarnessGlyph.test.tsx | 18 +++++++++ .../components/intelligence/HarnessGlyph.tsx | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 app/src/components/intelligence/HarnessGlyph.test.tsx create mode 100644 app/src/components/intelligence/HarnessGlyph.tsx diff --git a/app/src/components/intelligence/HarnessGlyph.test.tsx b/app/src/components/intelligence/HarnessGlyph.test.tsx new file mode 100644 index 0000000000..d06dd1a941 --- /dev/null +++ b/app/src/components/intelligence/HarnessGlyph.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import HarnessGlyph, { type GlyphKind } from './HarnessGlyph'; + +describe('HarnessGlyph', () => { + it.each<[GlyphKind, string]>([ + ['claude', 'C'], + ['codex', 'Cx'], + ['gemini', 'G'], + ['openhuman', 'OH'], + ])('renders the %s mark', (harness, label) => { + render(); + const glyph = screen.getByTestId('harness-glyph'); + expect(glyph).toHaveAttribute('data-harness', harness); + expect(glyph).toHaveTextContent(label); + }); +}); diff --git a/app/src/components/intelligence/HarnessGlyph.tsx b/app/src/components/intelligence/HarnessGlyph.tsx new file mode 100644 index 0000000000..212214f514 --- /dev/null +++ b/app/src/components/intelligence/HarnessGlyph.tsx @@ -0,0 +1,40 @@ +/** + * HarnessGlyph — a small brand mark for the agent harness driving an instance + * (Claude / Codex / Gemini), plus an OpenHuman mark for internal windows. Used + * as the leading glyph in roster rows. + * + * The colors here are deliberate brand/identity hues (not surface chrome), so + * they use literal palette classes per the project's "meaningful content color" + * guidance rather than semantic tokens. + */ +import type { HarnessType } from '../../lib/orchestration/orchestrationClient'; + +export type GlyphKind = HarnessType | 'openhuman'; + +export interface HarnessGlyphProps { + harness: GlyphKind; + className?: string; +} + +const GLYPH: Record = { + claude: { label: 'C', tone: 'bg-[#c96442] text-white' }, + codex: { label: 'Cx', tone: 'bg-content text-surface' }, + gemini: { label: 'G', tone: 'bg-ocean-500 text-white' }, + openhuman: { label: 'OH', tone: 'bg-sage-500 text-white' }, +}; + +export default function HarnessGlyph({ + harness, + className, +}: HarnessGlyphProps): React.ReactElement { + const { label, tone } = GLYPH[harness]; + return ( + + {label} + + ); +} From 04423782ad72a80391851fe20ae63cdcd72eaa6f Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:15 +0530 Subject: [PATCH 6/9] feat(intelligence): add InstanceCard roster row Harness glyph + status dot + identity (@handle/label/short address) + one-line current task + unread pill; selectable. Presentational. --- .../intelligence/InstanceCard.test.tsx | 56 ++++++++++++++ .../components/intelligence/InstanceCard.tsx | 74 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 app/src/components/intelligence/InstanceCard.test.tsx create mode 100644 app/src/components/intelligence/InstanceCard.tsx diff --git a/app/src/components/intelligence/InstanceCard.test.tsx b/app/src/components/intelligence/InstanceCard.test.tsx new file mode 100644 index 0000000000..ea7b01bbcc --- /dev/null +++ b/app/src/components/intelligence/InstanceCard.test.tsx @@ -0,0 +1,56 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import type { SessionSummary } from '../../lib/orchestration/orchestrationClient'; +import InstanceCard from './InstanceCard'; + +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +function session(over: Partial = {}): SessionSummary { + return { + sessionId: 'w1', + agentId: '6wNaBJkatir4B86cw5ykHZWQ3xoNaKygX5vAU9MQbHSh', + source: 'claude', + harnessType: 'claude', + status: 'idle', + currentTask: 'drafting hub cards', + chatKind: 'session', + lastMessageAt: '2026-07-06T00:00:00Z', + unread: 0, + active: true, + pinned: false, + ...over, + }; +} + +describe('InstanceCard', () => { + it('renders the harness glyph, status dot, task and shortened address', () => { + render(); + expect(screen.getByTestId('harness-glyph')).toHaveAttribute('data-harness', 'claude'); + expect(screen.getByTestId('instance-status-dot')).toHaveAttribute('data-status', 'idle'); + expect(screen.getByText('drafting hub cards')).toBeInTheDocument(); + expect(screen.getByText('6wNaBJ…QbHSh')).toBeInTheDocument(); + }); + + it('prefers a resolved @handle over the address', () => { + render(); + expect(screen.getByText('@claudebot')).toBeInTheDocument(); + }); + + it('shows the unread pill only when unread > 0 and fires onSelect', () => { + const onSelect = vi.fn(); + const { rerender } = render(); + expect(screen.queryByTestId('instance-card-unread')).toBeNull(); + + rerender(); + expect(screen.getByTestId('instance-card-unread')).toHaveTextContent('3'); + + fireEvent.click(screen.getByTestId('instance-card-w1')); + expect(onSelect).toHaveBeenCalledOnce(); + }); + + it('falls back to the OpenHuman glyph when no harness is set', () => { + render(); + expect(screen.getByTestId('harness-glyph')).toHaveAttribute('data-harness', 'openhuman'); + }); +}); diff --git a/app/src/components/intelligence/InstanceCard.tsx b/app/src/components/intelligence/InstanceCard.tsx new file mode 100644 index 0000000000..9210b1c5dd --- /dev/null +++ b/app/src/components/intelligence/InstanceCard.tsx @@ -0,0 +1,74 @@ +/** + * InstanceCard — one roster row for an agent instance: harness glyph + status + * dot + identity + one-line current task + unread pill. + * + * Presentational only; the parent supplies the {@link SessionSummary} and an + * optional resolved `@handle` (the raw address is the fallback). + */ +import { useT } from '../../lib/i18n/I18nContext'; +import type { InstanceStatus, SessionSummary } from '../../lib/orchestration/orchestrationClient'; +import HarnessGlyph, { type GlyphKind } from './HarnessGlyph'; +import InstanceStatusDot from './InstanceStatusDot'; + +export interface InstanceCardProps { + session: SessionSummary; + selected?: boolean; + onSelect?: () => void; + /** Resolved `@handle` for the peer, if known (address is the fallback). */ + handle?: string | null; +} + +const STATUS_LABEL_KEY: Record = { + running: 'tinyplaceOrchestration.status.running', + idle: 'tinyplaceOrchestration.status.idle', + 'waiting-approval': 'tinyplaceOrchestration.status.waitingApproval', + errored: 'tinyplaceOrchestration.status.errored', + stopped: 'tinyplaceOrchestration.status.stopped', +}; + +function shortAddress(address: string): string { + if (address.length <= 14) return address; + return `${address.slice(0, 6)}…${address.slice(-5)}`; +} + +export default function InstanceCard({ + session, + selected, + onSelect, + handle, +}: InstanceCardProps): React.ReactElement { + const { t } = useT(); + const glyph: GlyphKind = session.harnessType ?? 'openhuman'; + const identity = handle ? `@${handle}` : (session.label ?? shortAddress(session.agentId)); + + return ( + + ); +} From b3277040f42f447f5c98082e2c7fb08452fda5e0 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:16 +0530 Subject: [PATCH 7/9] feat(intelligence): add TinyPlaceRoster grouped by harness Instance roster grouping external agent sessions by harness (Claude/Codex/ Gemini, then an Other catch-all), each a selectable InstanceCard; excludes the pinned master/subconscious windows; empty state. Presentational. --- .../intelligence/TinyPlaceRoster.test.tsx | 69 ++++++++++++++++ .../intelligence/TinyPlaceRoster.tsx | 80 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 app/src/components/intelligence/TinyPlaceRoster.test.tsx create mode 100644 app/src/components/intelligence/TinyPlaceRoster.tsx diff --git a/app/src/components/intelligence/TinyPlaceRoster.test.tsx b/app/src/components/intelligence/TinyPlaceRoster.test.tsx new file mode 100644 index 0000000000..ecfd537c4f --- /dev/null +++ b/app/src/components/intelligence/TinyPlaceRoster.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import type { SessionSummary } from '../../lib/orchestration/orchestrationClient'; +import TinyPlaceRoster from './TinyPlaceRoster'; + +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +function session(over: Partial = {}): SessionSummary { + return { + sessionId: 's', + agentId: '@peer', + source: 'claude', + harnessType: 'claude', + status: 'idle', + chatKind: 'session', + lastMessageAt: '2026-07-06T00:00:00Z', + unread: 0, + active: true, + pinned: false, + ...over, + }; +} + +describe('TinyPlaceRoster', () => { + it('shows the empty state when there are no instance sessions', () => { + // Pinned windows are not instances and must not count. + const pinned = session({ sessionId: 'master', chatKind: 'master', pinned: true }); + render(); + expect(screen.getByTestId('tinyplace-roster-empty')).toBeInTheDocument(); + }); + + it('groups instances by harness and lists an Other group for harness-less sessions', () => { + const sessions = [ + session({ sessionId: 'c1', harnessType: 'claude' }), + session({ sessionId: 'x1', harnessType: 'codex', source: 'codex' }), + session({ sessionId: 'u1', harnessType: undefined, source: 'user_created' }), + ]; + render(); + expect(screen.getByText('Claude')).toBeInTheDocument(); + expect(screen.getByText('Codex')).toBeInTheDocument(); + // Harness-less session lands under the (translated) Other group; no empty Gemini group. + expect(screen.getByText('tinyplaceOrchestration.roster.other')).toBeInTheDocument(); + expect(screen.queryByText('Gemini')).toBeNull(); + expect(screen.getByTestId('instance-card-c1')).toBeInTheDocument(); + expect(screen.getByTestId('instance-card-u1')).toBeInTheDocument(); + }); + + it('marks the selected instance and forwards selection', () => { + const onSelect = vi.fn(); + const sessions = [session({ sessionId: 'c1' }), session({ sessionId: 'c2' })]; + render(); + expect(screen.getByTestId('instance-card-c1')).toHaveAttribute('data-selected', 'true'); + expect(screen.getByTestId('instance-card-c2')).toHaveAttribute('data-selected', 'false'); + fireEvent.click(screen.getByTestId('instance-card-c2')); + expect(onSelect).toHaveBeenCalledWith('c2'); + }); + + it('passes resolved handles down to the cards', () => { + render( + + ); + const card = screen.getByTestId('instance-card-c1'); + expect(within(card).getByText('@claudebot')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/intelligence/TinyPlaceRoster.tsx b/app/src/components/intelligence/TinyPlaceRoster.tsx new file mode 100644 index 0000000000..709b15d2e9 --- /dev/null +++ b/app/src/components/intelligence/TinyPlaceRoster.tsx @@ -0,0 +1,80 @@ +/** + * TinyPlaceRoster — the instance roster: external agent sessions grouped by + * harness (Claude / Codex / Gemini, then an "Other" catch-all), each a + * selectable {@link InstanceCard}. Pinned master/subconscious windows are not + * instances and are excluded here. + * + * Presentational only; the parent passes the session list, selection, and an + * optional address→@handle map. + */ +import { useT } from '../../lib/i18n/I18nContext'; +import type { HarnessType, SessionSummary } from '../../lib/orchestration/orchestrationClient'; +import InstanceCard from './InstanceCard'; + +export interface TinyPlaceRosterProps { + sessions: SessionSummary[]; + selectedId?: string; + onSelect?: (sessionId: string) => void; + /** Resolved address → `@handle` map (best-effort; address is the fallback). */ + handles?: Record; +} + +// Grouped in this order; brand names are identity, not translated UI copy. +const HARNESS_GROUPS: Array<{ key: HarnessType; label: string }> = [ + { key: 'claude', label: 'Claude' }, + { key: 'codex', label: 'Codex' }, + { key: 'gemini', label: 'Gemini' }, +]; + +export default function TinyPlaceRoster({ + sessions, + selectedId, + onSelect, + handles, +}: TinyPlaceRosterProps): React.ReactElement { + const { t } = useT(); + + // Instances are the non-pinned session windows. + const instances = sessions.filter(s => !s.pinned && s.chatKind === 'session'); + + const byHarness = (harness: HarnessType): SessionSummary[] => + instances.filter(s => s.harnessType === harness); + const ungrouped = instances.filter(s => !s.harnessType); + + const groups = [ + ...HARNESS_GROUPS.map(g => ({ label: g.label, rows: byHarness(g.key) })), + { label: t('tinyplaceOrchestration.roster.other'), rows: ungrouped }, + ].filter(g => g.rows.length > 0); + + return ( +
+

+ {t('tinyplaceOrchestration.roster.instances')} +

+ {instances.length === 0 ? ( +

+ {t('tinyplaceOrchestration.roster.empty')} +

+ ) : ( + groups.map(group => ( +
+
+ {group.label} +
+ {group.rows.map(session => ( + onSelect(session.sessionId) : undefined} + /> + ))} +
+ )) + )} +
+ ); +} From b52f6bbe243ca764608778480a3d72aa07d80afb Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:16 +0530 Subject: [PATCH 8/9] test(intelligence): add status to tab test session fixtures SessionSummary now requires status; backfill the existing tab-test fixtures. --- .../intelligence/TinyPlaceOrchestrationTab.test.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/components/intelligence/TinyPlaceOrchestrationTab.test.tsx b/app/src/components/intelligence/TinyPlaceOrchestrationTab.test.tsx index 49c0b66b91..8a82490865 100644 --- a/app/src/components/intelligence/TinyPlaceOrchestrationTab.test.tsx +++ b/app/src/components/intelligence/TinyPlaceOrchestrationTab.test.tsx @@ -69,6 +69,7 @@ const PINNED_SESSIONS = [ unread: 0, active: true, pinned: true, + status: 'idle' as const, }, { sessionId: 'subconscious', @@ -79,6 +80,7 @@ const PINNED_SESSIONS = [ unread: 0, active: true, pinned: true, + status: 'idle' as const, }, ]; @@ -96,6 +98,7 @@ describe('TinyPlaceOrchestrationTab', () => { unread: 0, active: true, pinned: false, + status: 'idle' as const, }, }); messagesListMock.mockResolvedValue({ messages: [] }); @@ -175,6 +178,7 @@ describe('TinyPlaceOrchestrationTab', () => { unread: 0, active: true, pinned: false, + status: 'idle' as const, }, ], }); @@ -201,6 +205,7 @@ describe('TinyPlaceOrchestrationTab', () => { unread: 0, active: true, pinned: false, + status: 'idle' as const, }, ], }); @@ -254,6 +259,7 @@ describe('TinyPlaceOrchestrationTab', () => { unread: 3, active: true, pinned: false, + status: 'idle' as const, }, ], }); @@ -499,6 +505,7 @@ describe('TinyPlaceOrchestrationTab', () => { unread: 0, active: true, pinned: false, + status: 'idle' as const, }, ], }); From aadbab8a98999e1409ae8bf6d994b71fdb164433 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 6 Jul 2026 22:20:22 +0530 Subject: [PATCH 9/9] feat(i18n): add roster + instance-status strings 8 keys (tinyplaceOrchestration.roster.* + .status.*) with real translations across en + 13 locales for the roster and InstanceCard status labels. --- app/src/lib/i18n/ar.ts | 8 ++++++++ app/src/lib/i18n/bn.ts | 8 ++++++++ app/src/lib/i18n/de.ts | 8 ++++++++ app/src/lib/i18n/en.ts | 8 ++++++++ app/src/lib/i18n/es.ts | 8 ++++++++ app/src/lib/i18n/fr.ts | 8 ++++++++ app/src/lib/i18n/hi.ts | 8 ++++++++ app/src/lib/i18n/id.ts | 8 ++++++++ app/src/lib/i18n/it.ts | 8 ++++++++ app/src/lib/i18n/ko.ts | 8 ++++++++ app/src/lib/i18n/pl.ts | 8 ++++++++ app/src/lib/i18n/pt.ts | 8 ++++++++ app/src/lib/i18n/ru.ts | 8 ++++++++ app/src/lib/i18n/zh-CN.ts | 8 ++++++++ 14 files changed, 112 insertions(+) diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index f4df8f1b22..d63a7b649b 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -288,6 +288,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'إرسال', 'tinyplaceOrchestration.composer.sendFailed': 'فشل إرسال الرسالة', 'tinyplaceOrchestration.steering.label': 'التوجيه', + 'tinyplaceOrchestration.roster.instances': 'مثيلات', + 'tinyplaceOrchestration.roster.empty': 'لا توجد مثيلات وكيل بعد', + 'tinyplaceOrchestration.roster.other': 'أخرى', + 'tinyplaceOrchestration.status.running': 'قيد التشغيل', + 'tinyplaceOrchestration.status.idle': 'خامل', + 'tinyplaceOrchestration.status.waitingApproval': 'في انتظار الموافقة', + 'tinyplaceOrchestration.status.errored': 'خطأ', + 'tinyplaceOrchestration.status.stopped': 'متوقف', 'brain.empty': 'دماغك فارغ في الوقت الحالي — قم بربط مصدر لبدء بناء الذاكرة.', 'brain.error': 'تعذّر تحميل دماغك. يرجى المحاولة مرة أخرى.', 'common.cancel': 'إلغاء', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 6529ed035b..71278320d7 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -295,6 +295,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'পাঠান', 'tinyplaceOrchestration.composer.sendFailed': 'বার্তা পাঠাতে ব্যর্থ', 'tinyplaceOrchestration.steering.label': 'নির্দেশনা', + 'tinyplaceOrchestration.roster.instances': 'ইনস্ট্যান্স', + 'tinyplaceOrchestration.roster.empty': 'এখনও কোনো এজেন্ট ইনস্ট্যান্স নেই', + 'tinyplaceOrchestration.roster.other': 'অন্যান্য', + 'tinyplaceOrchestration.status.running': 'চলছে', + 'tinyplaceOrchestration.status.idle': 'নিষ্ক্রিয়', + 'tinyplaceOrchestration.status.waitingApproval': 'অনুমোদনের অপেক্ষায়', + 'tinyplaceOrchestration.status.errored': 'ত্রুটিপূর্ণ', + 'tinyplaceOrchestration.status.stopped': 'থেমে গেছে', 'brain.empty': 'আপনার ব্রেইন এখন খালি — মেমরি তৈরি শুরু করতে একটি উৎস সংযুক্ত করুন।', 'brain.error': 'আপনার ব্রেইন লোড করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।', 'common.cancel': 'বাতিল', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 8086d78728..c904c63b6c 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -302,6 +302,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Senden', 'tinyplaceOrchestration.composer.sendFailed': 'Nachricht konnte nicht gesendet werden', 'tinyplaceOrchestration.steering.label': 'Steuerung', + 'tinyplaceOrchestration.roster.instances': 'Instanzen', + 'tinyplaceOrchestration.roster.empty': 'Noch keine Agent-Instanzen', + 'tinyplaceOrchestration.roster.other': 'Andere', + 'tinyplaceOrchestration.status.running': 'Läuft', + 'tinyplaceOrchestration.status.idle': 'Inaktiv', + 'tinyplaceOrchestration.status.waitingApproval': 'Warten auf Freigabe', + 'tinyplaceOrchestration.status.errored': 'Fehler', + 'tinyplaceOrchestration.status.stopped': 'Gestoppt', 'brain.empty': 'Dein Gehirn ist noch leer – verbinde eine Quelle, um Speicher aufzubauen.', 'brain.error': 'Dein Gehirn konnte nicht geladen werden. Bitte versuche es erneut.', 'common.cancel': 'Abbrechen', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index e5f753db45..674424bf26 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -4223,6 +4223,14 @@ const en: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Send', 'tinyplaceOrchestration.composer.sendFailed': 'Failed to send message', 'tinyplaceOrchestration.steering.label': 'Steering', + 'tinyplaceOrchestration.roster.instances': 'Instances', + 'tinyplaceOrchestration.roster.empty': 'No agent instances yet', + 'tinyplaceOrchestration.roster.other': 'Other', + 'tinyplaceOrchestration.status.running': 'Running', + 'tinyplaceOrchestration.status.idle': 'Idle', + 'tinyplaceOrchestration.status.waitingApproval': 'Waiting for approval', + 'tinyplaceOrchestration.status.errored': 'Errored', + 'tinyplaceOrchestration.status.stopped': 'Stopped', 'intelligence.teams.subtitle': 'Coordinated agent teams and the tasks they share.', 'intelligence.teams.loading': 'Loading teams…', 'intelligence.teams.failedToLoad': 'Failed to load teams', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 72456f24bc..1336cecdea 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -299,6 +299,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Enviar', 'tinyplaceOrchestration.composer.sendFailed': 'No se pudo enviar el mensaje', 'tinyplaceOrchestration.steering.label': 'Dirección', + 'tinyplaceOrchestration.roster.instances': 'Instancias', + 'tinyplaceOrchestration.roster.empty': 'Aún no hay instancias de agente', + 'tinyplaceOrchestration.roster.other': 'Otros', + 'tinyplaceOrchestration.status.running': 'En ejecución', + 'tinyplaceOrchestration.status.idle': 'Inactivo', + 'tinyplaceOrchestration.status.waitingApproval': 'Esperando aprobación', + 'tinyplaceOrchestration.status.errored': 'Con error', + 'tinyplaceOrchestration.status.stopped': 'Detenido', 'brain.empty': 'Tu cerebro está vacío por ahora: conecta una fuente para empezar a construir tu memoria.', 'brain.error': 'No se pudo cargar tu cerebro. Inténtalo de nuevo.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index d1406b07d0..4c26d11f56 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -299,6 +299,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Envoyer', 'tinyplaceOrchestration.composer.sendFailed': 'Échec de l’envoi du message', 'tinyplaceOrchestration.steering.label': 'Pilotage', + 'tinyplaceOrchestration.roster.instances': 'Instances', + 'tinyplaceOrchestration.roster.empty': 'Aucune instance d’agent', + 'tinyplaceOrchestration.roster.other': 'Autre', + 'tinyplaceOrchestration.status.running': 'En cours', + 'tinyplaceOrchestration.status.idle': 'Inactif', + 'tinyplaceOrchestration.status.waitingApproval': 'En attente d’approbation', + 'tinyplaceOrchestration.status.errored': 'En erreur', + 'tinyplaceOrchestration.status.stopped': 'Arrêté', 'brain.empty': 'Votre cerveau est vide pour l’instant — connectez une source pour commencer à constituer votre mémoire.', 'brain.error': 'Impossible de charger votre cerveau. Veuillez réessayer.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index b98fb1ae93..0b0f6fc7ef 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -295,6 +295,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'भेजें', 'tinyplaceOrchestration.composer.sendFailed': 'संदेश भेजने में विफल', 'tinyplaceOrchestration.steering.label': 'मार्गदर्शन', + 'tinyplaceOrchestration.roster.instances': 'इंस्टेंस', + 'tinyplaceOrchestration.roster.empty': 'अभी तक कोई एजेंट इंस्टेंस नहीं', + 'tinyplaceOrchestration.roster.other': 'अन्य', + 'tinyplaceOrchestration.status.running': 'चल रहा है', + 'tinyplaceOrchestration.status.idle': 'निष्क्रिय', + 'tinyplaceOrchestration.status.waitingApproval': 'अनुमोदन की प्रतीक्षा में', + 'tinyplaceOrchestration.status.errored': 'त्रुटि', + 'tinyplaceOrchestration.status.stopped': 'रुका हुआ', 'brain.empty': 'आपका ब्रेन अभी खाली है — मेमोरी बनाना शुरू करने के लिए कोई स्रोत कनेक्ट करें।', 'brain.error': 'आपका ब्रेन लोड नहीं हो सका। कृपया फिर से प्रयास करें।', 'common.cancel': 'रद्द करें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 622dffcf23..a75a182ab4 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -296,6 +296,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Kirim', 'tinyplaceOrchestration.composer.sendFailed': 'Gagal mengirim pesan', 'tinyplaceOrchestration.steering.label': 'Pengarahan', + 'tinyplaceOrchestration.roster.instances': 'Instansi', + 'tinyplaceOrchestration.roster.empty': 'Belum ada instansi agen', + 'tinyplaceOrchestration.roster.other': 'Lainnya', + 'tinyplaceOrchestration.status.running': 'Berjalan', + 'tinyplaceOrchestration.status.idle': 'Diam', + 'tinyplaceOrchestration.status.waitingApproval': 'Menunggu persetujuan', + 'tinyplaceOrchestration.status.errored': 'Bermasalah', + 'tinyplaceOrchestration.status.stopped': 'Berhenti', 'brain.empty': 'Otak Anda masih kosong — hubungkan sumber untuk mulai membangun memori.', 'brain.error': 'Tidak dapat memuat otak Anda. Silakan coba lagi.', 'common.cancel': 'Batal', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index c436e94a62..9f28ae5fc9 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -299,6 +299,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Invia', 'tinyplaceOrchestration.composer.sendFailed': 'Invio del messaggio non riuscito', 'tinyplaceOrchestration.steering.label': 'Direzione', + 'tinyplaceOrchestration.roster.instances': 'Istanze', + 'tinyplaceOrchestration.roster.empty': 'Nessuna istanza agente', + 'tinyplaceOrchestration.roster.other': 'Altro', + 'tinyplaceOrchestration.status.running': 'In esecuzione', + 'tinyplaceOrchestration.status.idle': 'Inattivo', + 'tinyplaceOrchestration.status.waitingApproval': 'In attesa di approvazione', + 'tinyplaceOrchestration.status.errored': 'In errore', + 'tinyplaceOrchestration.status.stopped': 'Arrestato', 'brain.empty': 'Il tuo cervello è ancora vuoto: collega una fonte per iniziare a costruire la memoria.', 'brain.error': 'Impossibile caricare il tuo cervello. Riprova.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 8f004c741f..75dc649abb 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -292,6 +292,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': '보내기', 'tinyplaceOrchestration.composer.sendFailed': '메시지를 보내지 못했습니다', 'tinyplaceOrchestration.steering.label': '조종', + 'tinyplaceOrchestration.roster.instances': '인스턴스', + 'tinyplaceOrchestration.roster.empty': '아직 에이전트 인스턴스가 없습니다', + 'tinyplaceOrchestration.roster.other': '기타', + 'tinyplaceOrchestration.status.running': '실행 중', + 'tinyplaceOrchestration.status.idle': '유휴', + 'tinyplaceOrchestration.status.waitingApproval': '승인 대기 중', + 'tinyplaceOrchestration.status.errored': '오류', + 'tinyplaceOrchestration.status.stopped': '중지됨', 'brain.empty': '아직 브레인이 비어 있습니다 — 소스를 연결하여 메모리를 만들어 보세요.', 'brain.error': '브레인을 불러올 수 없습니다. 다시 시도해 주세요.', 'common.cancel': '취소', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 5046645278..4e790064c1 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -300,6 +300,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Wyślij', 'tinyplaceOrchestration.composer.sendFailed': 'Nie udało się wysłać wiadomości', 'tinyplaceOrchestration.steering.label': 'Sterowanie', + 'tinyplaceOrchestration.roster.instances': 'Instancje', + 'tinyplaceOrchestration.roster.empty': 'Brak instancji agentów', + 'tinyplaceOrchestration.roster.other': 'Inne', + 'tinyplaceOrchestration.status.running': 'Działa', + 'tinyplaceOrchestration.status.idle': 'Bezczynny', + 'tinyplaceOrchestration.status.waitingApproval': 'Oczekuje na zatwierdzenie', + 'tinyplaceOrchestration.status.errored': 'Błąd', + 'tinyplaceOrchestration.status.stopped': 'Zatrzymany', 'brain.empty': 'Twój mózg jest na razie pusty — połącz źródło, aby zacząć budować pamięć.', 'brain.error': 'Nie udało się załadować Twojego mózgu. Spróbuj ponownie.', 'common.cancel': 'Anuluj', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 70955a167b..898144831a 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -298,6 +298,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Enviar', 'tinyplaceOrchestration.composer.sendFailed': 'Falha ao enviar mensagem', 'tinyplaceOrchestration.steering.label': 'Direção', + 'tinyplaceOrchestration.roster.instances': 'Instâncias', + 'tinyplaceOrchestration.roster.empty': 'Nenhuma instância de agente ainda', + 'tinyplaceOrchestration.roster.other': 'Outros', + 'tinyplaceOrchestration.status.running': 'Em execução', + 'tinyplaceOrchestration.status.idle': 'Inativo', + 'tinyplaceOrchestration.status.waitingApproval': 'Aguardando aprovação', + 'tinyplaceOrchestration.status.errored': 'Com erro', + 'tinyplaceOrchestration.status.stopped': 'Parado', 'brain.empty': 'Seu cérebro está vazio por enquanto — conecte uma fonte para começar a construir a memória.', 'brain.error': 'Não foi possível carregar seu cérebro. Tente novamente.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 6c08997019..125ff4671e 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -300,6 +300,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': 'Отправить', 'tinyplaceOrchestration.composer.sendFailed': 'Не удалось отправить сообщение', 'tinyplaceOrchestration.steering.label': 'Управление', + 'tinyplaceOrchestration.roster.instances': 'Экземпляры', + 'tinyplaceOrchestration.roster.empty': 'Пока нет экземпляров агентов', + 'tinyplaceOrchestration.roster.other': 'Другое', + 'tinyplaceOrchestration.status.running': 'Выполняется', + 'tinyplaceOrchestration.status.idle': 'Простаивает', + 'tinyplaceOrchestration.status.waitingApproval': 'Ожидает подтверждения', + 'tinyplaceOrchestration.status.errored': 'Ошибка', + 'tinyplaceOrchestration.status.stopped': 'Остановлен', 'brain.empty': 'Ваш мозг пока пуст — подключите источник, чтобы начать формировать память.', 'brain.error': 'Не удалось загрузить ваш мозг. Пожалуйста, попробуйте ещё раз.', 'common.cancel': 'Отмена', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index d9fbe10367..8bcafc236c 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -276,6 +276,14 @@ const messages: TranslationMap = { 'tinyplaceOrchestration.composer.send': '发送', 'tinyplaceOrchestration.composer.sendFailed': '消息发送失败', 'tinyplaceOrchestration.steering.label': '引导', + 'tinyplaceOrchestration.roster.instances': '实例', + 'tinyplaceOrchestration.roster.empty': '暂无代理实例', + 'tinyplaceOrchestration.roster.other': '其他', + 'tinyplaceOrchestration.status.running': '运行中', + 'tinyplaceOrchestration.status.idle': '空闲', + 'tinyplaceOrchestration.status.waitingApproval': '等待批准', + 'tinyplaceOrchestration.status.errored': '出错', + 'tinyplaceOrchestration.status.stopped': '已停止', 'brain.empty': '你的大脑暂时是空的——连接一个来源即可开始构建记忆。', 'brain.error': '无法加载你的大脑,请重试。', 'common.cancel': '取消',