Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ export const chatStatusSchema = z.object({
goals: z.array(chatGoalSchema),
});

export const managerChannelBindingSchema = z.object({
schema_version: z.string(),
executor_endpoint: z.string(),
executor_endpoint_source: z.string(),
executor_kind: z.string(),
model: z.string(),
model_source: z.string(),
credential_env_var: z.string(),
operator_credential_configured: z.boolean(),
available: z.boolean().nullable(),
unavailable_reason: z.string().nullable(),
});

export type ManagerChannelBinding = z.infer<typeof managerChannelBindingSchema>;

export const chatCapabilitiesSchema = z.object({
ok: z.literal(true),
schema_version: z.enum(["loopx_chat_capabilities_v0", "loopx_chat_capabilities_v1"]),
Expand All @@ -109,6 +124,7 @@ export const chatCapabilitiesSchema = z.object({
scope: z.literal("owner_global"),
model: z.string(),
reasoning_effort: z.string(),
channel_binding: managerChannelBindingSchema.optional(),
runtime: z.object({
schema_version: z.literal("manager_runtime_effective_profile_v0"),
runtime_profile: z.enum(["restricted", "trusted_owner"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import { Bot, ChevronDown, Eye, Info, Menu, RefreshCw, SlidersHorizontal } from "lucide-react";

import { localizedGoalState, useWorkspaceI18n } from "./i18n";
import type { ManagerRuntimeSessionReadback } from "../../data/chat";
import type { ManagerChannelBinding, ManagerRuntimeSessionReadback } from "../../data/chat";
import type { WorkspaceAgentOption, WorkspaceGoal, WorkspaceGoalTab } from "./personal-workspace-model";
import { goalUsageLabel } from "./personal-workspace-model";
import { WorkspaceSelect } from "./workspace-select";

export function ChannelHeader({
agents,
managerChannelBinding,
managerChatOpen,
managerRuntime,
mobileNavigationOpen,
Expand All @@ -27,6 +28,7 @@
selectedGoalTab,
}: {
agents: WorkspaceAgentOption[];
managerChannelBinding?: ManagerChannelBinding | null;
managerChatOpen?: boolean;
managerRuntime?: ManagerRuntimeSessionReadback | null;
mobileNavigationOpen?: boolean;
Expand Down Expand Up @@ -84,6 +86,17 @@
tokens: t("drawer.tokensShort"),
})
: null;
// The chip reports the selected executor, whose credential pays for it, and
// the resolved model, so an executor and a model that disagree are visible
// instead of arriving as one silent configuration.
const managerExecutionKindLabel = managerChannelBinding
? managerChannelBinding.executor_kind === "individual"
? t("header.managerExecutorKindIndividual")
: managerChannelBinding.executor_kind === "managed"
? t("header.managerExecutorKindManaged")
: t("header.managerExecutorKindRegistered")

Check warning on line 97 in apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaClfnAiothckwG6ls87&open=AaClfnAiothckwG6ls87&pullRequest=4419

Check warning on line 97 in apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaClfnAjothckwG6ls88&open=AaClfnAjothckwG6ls88&pullRequest=4419
: null;
const managerExecutionUnavailable = managerChannelBinding?.available === false;

return (
<header className="personal-channel-header">
Expand All @@ -101,6 +114,20 @@
sandbox: managerRuntime.sandbox,
})}</p>
) : null}
{!selectedGoal && managerChannelBinding ? (
<p className="personal-manager-execution">
<span className={managerExecutionUnavailable ? "personal-execution-chip is-unavailable" : "personal-execution-chip"}>
<span className="personal-execution-chip-endpoint">{managerChannelBinding.executor_endpoint}</span>
{managerExecutionKindLabel ? <span className="personal-execution-chip-kind">{managerExecutionKindLabel}</span> : null}
<span className="personal-execution-chip-model">{managerChannelBinding.model}</span>
</span>
{managerExecutionUnavailable ? (
<span className="personal-execution-note">
{t("header.managerExecutionUnavailable", { executor: managerChannelBinding.executor_endpoint })}
</span>
) : null}
</p>
) : null}
{selectedGoal ? <p>{selectedGoal.loadState ? t(selectedGoal.loadState === "error" ? "startup.goalError" : "startup.goalLoading") : `${selectedGoal.agentLaneCount && selectedGoal.agentLaneCount > 1
? t("header.workAgentCount", { count: selectedGoal.agentLaneCount })
: selectedGoal.agentLabel ?? selectedGoal.agentId} · ${(selectedGoal.loadState ? t(selectedGoal.loadState === "error" ? "startup.goalError" : "startup.goalLoading") : localizedGoalState(selectedGoal.state, locale))}${selectedGoalUsageLabel ? ` · ${selectedGoalUsageLabel}` : ""} · ${selectedGoal.nextSentence}`}</p> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ const en = {
"header.managerDescription": "Your personal workspace across Goals",
"header.managerRuntime": "{profile} · {sandbox}",
"header.managerRuntimeFallback": "Configuration invalid; fell back to {profile} · {sandbox}. Repair it in Machine capabilities.",
"header.managerExecutorKindIndividual": "individual CLI login",
"header.managerExecutorKindManaged": "operator credential",
"header.managerExecutorKindRegistered": "registered endpoint",
"header.managerExecutionUnavailable": "Selected {executor} has no Chat transport yet; this channel needs codex, or run the managed host through loopx turn.",
"header.managerOverview": "Overview",
"header.managerView": "Manager view",
"header.openGoalNavigation": "Open Goal navigation",
Expand Down Expand Up @@ -1405,6 +1409,10 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"header.managerDescription": "跨 Goal 的个人工作入口",
"header.managerRuntime": "{profile} · {sandbox}",
"header.managerRuntimeFallback": "配置无效,已回退到 {profile} · {sandbox};请在机器能力设置中修复。",
"header.managerExecutorKindIndividual": "个人 CLI 登录",
"header.managerExecutorKindManaged": "operator 凭据",
"header.managerExecutorKindRegistered": "注册端点",
"header.managerExecutionUnavailable": "所选执行器 {executor} 尚无 Chat 通道;管家通道需要 codex,托管宿主请走 loopx turn。",
"header.managerOverview": "总览",
"header.managerView": "管家视图",
"header.openGoalNavigation": "打开 Goal 导航",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
transitionTypedAction,
type GoalRepositoryContext,
type LarkGoalConnection,
type ManagerChannelBinding,
type ManagerRuntimeSessionReadback,
type TypedActionProposal,
} from "../../data/chat";
Expand Down Expand Up @@ -751,6 +752,7 @@ export function PersonalWorkspacePage({
agents = [{ agentId: "codex", available: true, capability: "代码与项目执行", label: "Codex" }],
callbacks = {},
goalArchiveLoadState = { error: null, phase: "ready" },
managerChannelBinding,
managerRuntime,
model,
readOnly = false,
Expand All @@ -761,6 +763,7 @@ export function PersonalWorkspacePage({
agents?: WorkspaceAgentOption[];
callbacks?: PersonalWorkspaceCallbacks;
goalArchiveLoadState?: WorkspaceGoalArchiveLoadState;
managerChannelBinding?: ManagerChannelBinding | null;
managerRuntime?: ManagerRuntimeSessionReadback | null;
model: WorkspaceModel;
ownerLabel?: string;
Expand Down Expand Up @@ -1830,6 +1833,7 @@ export function PersonalWorkspacePage({
<ChannelHeader
agents={agents}
managerChatOpen={managerChatOpen}
managerChannelBinding={managerChannelBinding}
managerRuntime={managerRuntime}
mobileNavigationOpen={mobileSidebarOpen}
onOpenGoalCapabilities={selectedGoal ? () => setSelection({ goalId: selectedGoal.goalId, kind: "settings", tab: "capabilities" }) : undefined}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
type ChatSessionSummary,
type ChatImageAttachment,
type ChatVisibleMessage,
type ManagerChannelBinding,
type ManagerRuntimeSessionReadback,
type ProtectedActionProposal,
type TodoProposal,
Expand Down Expand Up @@ -1399,6 +1400,7 @@ function PersonalGoalHome({
}>>([]);
const [goalSubagentConfigurationEnabled, setGoalSubagentConfigurationEnabled] = useState(false);
const [managerRuntime, setManagerRuntime] = useState<ManagerRuntimeSessionReadback | null>(null);
const [managerChannelBinding, setManagerChannelBinding] = useState<ManagerChannelBinding | null>(null);
const model = useMemo(() => {
const base = buildPersonalHomeModel(payload, rows, t, goalSubagentConfigurationEnabled);
if (!progress) return base;
Expand Down Expand Up @@ -1636,6 +1638,7 @@ function PersonalGoalHome({
setRuntimeAgents([]);
setGoalSubagentConfigurationEnabled(false);
setManagerRuntime(null);
setManagerChannelBinding(null);
return;
}
let cancelled = false;
Expand All @@ -1644,6 +1647,7 @@ function PersonalGoalHome({
if (!cancelled) {
setRuntimeAgents(capabilities.adapters ?? []);
const runtime = capabilities.manager?.runtime;
setManagerChannelBinding(capabilities.manager?.channel_binding ?? null);
setManagerRuntime(runtime ? {
schema_version: "manager_runtime_session_readback_v0",
runtime_profile: runtime.runtime_profile,
Expand Down Expand Up @@ -2831,6 +2835,7 @@ function PersonalGoalHome({
onStartNewRunSession: startNewManagerSession,
}}
goalArchiveLoadState={goalArchiveLoadState}
managerChannelBinding={managerChannelBinding}
managerRuntime={managerRuntime}
model={workspaceModel}
readOnly={readOnly}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions docs/reference/protocols/manager-evidence-and-continuity-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ and its source, whether an operator credential is configured, and
serve this channel. `available` is `null` when the projection makes no claim. A
frontend can show which executor and model the steward channel resolved, and why,
without re-deriving the rule.

The Personal Workspace manager header renders that binding as one compact chip
(`executor · executor kind · model`). The chip is display-only: it reads the
projected fields, keeps the executor kind in the same `individual`/`managed`
vocabulary, and names an unrecognized kind as an unclaimed registered endpoint
rather than guessing. When the projection proves the selected endpoint cannot
serve the channel, the chip is marked unavailable and the header states why in
the same row. A capabilities payload without `channel_binding` renders the
previous header unchanged.

| Surface, Chinese | Shipped selection | Managed host selected |
| --- | --- | --- |
| Desktop, 1512px | [Chip](../../assets/personal-workspace/steward-execution-chip-desktop.png) | [Unavailable](../../assets/personal-workspace/steward-execution-chip-unavailable-desktop.png) |
| Mobile, 390px | -- | [Unavailable](../../assets/personal-workspace/steward-execution-chip-unavailable-mobile.png) |

Legacy managed manager sessions retain their logical identity and bounded chat
history but start a fresh executor thread in the same Codex home on first
restore. This removes inherited project instructions without importing sessions
Expand Down
3 changes: 2 additions & 1 deletion examples/personal-workspace-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "./dashboard-browser-smoke-support.mjs";
import { writeDashboardBrowserCoverage } from "./dashboard-browser-coverage.mjs";
import { chatRecoveryScenario } from "./personal-workspace-browser/chat-recovery.mjs";
import { executionChipScenario } from "./personal-workspace-browser/execution-chip.mjs";
import {
collectCoverage,
dashboardDir,
Expand All @@ -23,7 +24,7 @@ import {
import { navigationSortingScenario } from "./personal-workspace-browser/navigation-sorting.mjs";
import { typedActionsScenario } from "./personal-workspace-browser/typed-actions.mjs";

const scenarioCatalog = [navigationSortingScenario, chatRecoveryScenario, typedActionsScenario];
const scenarioCatalog = [navigationSortingScenario, chatRecoveryScenario, typedActionsScenario, executionChipScenario];
const requestedScenario = process.env.LOOPX_PERSONAL_WORKSPACE_SCENARIO;
const scenarios = requestedScenario
? scenarioCatalog.filter((scenario) => scenario.id === requestedScenario)
Expand Down
Loading