返回 DeepSeek-Reasonix
remoteStatus.ts
根目录 / desktop / frontend / src / lib / remoteStatus.ts
1 import { normalizeToolApprovalMode, type CheckpointMeta, type EffortInfo, type GoalLifecycleView, type GoalRuntime, type GoalStatus, type QualityFloor, type ToolApprovalMode } from "./types";
2
3 // Raw /status payload mapping for the remote session surface. The serve reports
4 // the fields it knows; everything else stays undefined so callers keep prior
5 // values. These helpers are pure shape guards shared by the hydration,
6 // reconciliation, and watchdog paths in useRemoteSession.
7
8 // remoteStatusToAction maps the serve's raw /status payload onto the shared
9 // backend_status action so the remote surface reuses the local tab's running
10 // reconciliation (including its staleness guards). The serve reports the
11 // fields it knows; the rest stay undefined and the reducer keeps prior values.
12 export function remoteStatusToAction(status: unknown, snapshotAt: number, previousRunning = false) {
13 const raw = (status ?? null) as { running?: unknown; pendingPrompt?: unknown; backgroundJobs?: unknown; cancelRequested?: unknown; cancellable?: unknown } | null;
14 return {
15 type: "backend_status" as const,
16 running: typeof raw?.running === "boolean" ? raw.running : previousRunning,
17 pendingPrompt: raw?.pendingPrompt === undefined ? undefined : raw.pendingPrompt === true,
18 backgroundJobs: typeof raw?.backgroundJobs === "number" ? raw.backgroundJobs : undefined,
19 cancelRequested: raw?.cancelRequested === undefined ? undefined : raw.cancelRequested === true,
20 cancellable: raw?.cancellable === undefined ? undefined : raw.cancellable === true,
21 snapshotAt,
22 };
23 }
24
25 export type RemoteStatus = {
26 running?: unknown;
27 pendingPrompt?: unknown;
28 label?: unknown;
29 plan?: unknown;
30 toolApprovalMode?: unknown;
31 goal?: unknown;
32 goalStatus?: unknown;
33 goalView?: unknown;
34 effort?: unknown;
35 used?: unknown;
36 window?: unknown;
37 cacheHit?: unknown;
38 cacheMiss?: unknown;
39 lastUsage?: unknown;
40 balance?: unknown;
41 sessionCostQuote?: unknown;
42 jobs?: unknown;
43 qualityFloor?: unknown;
44 sessionName?: unknown;
45 goalRuntime?: unknown;
46 /** Ownership flag for sessions a local runtime on the serve host holds. */
47 takenOver?: unknown;
48 };
49
50 export function isAuthoritativeRemoteStatus(status: unknown): status is RemoteStatus {
51 if (!status || typeof status !== "object" || Array.isArray(status)) return false;
52 const raw = status as RemoteStatus;
53 return typeof raw.plan === "boolean"
54 && ["read-only", "workspace-write", "danger-full-access", "ask", "auto", "yolo"].includes(String(raw.toolApprovalMode))
55 && typeof raw.goal === "string";
56 }
57
58 /** The serve's ownership verdict; readable even from a non-authoritative status. */
59 export function remoteStatusTakenOver(status: unknown): boolean {
60 return Boolean(status && typeof status === "object" && !Array.isArray(status) && (status as RemoteStatus).takenOver === true);
61 }
62
63 export function remoteGoalView(status: unknown): GoalLifecycleView | undefined {
64 const value = (status as RemoteStatus | null)?.goalView;
65 if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
66 const raw = value as Partial<GoalLifecycleView>;
67 if (typeof raw.id !== "string" || typeof raw.revision !== "number" || typeof raw.objective !== "string"
68 || !["active", "paused", "blocked", "complete"].includes(String(raw.phase))
69 || !["armed", "disarmed"].includes(String(raw.activation)) || typeof raw.roundsStarted !== "number") return undefined;
70 return raw as GoalLifecycleView;
71 }
72
73 export function remoteComposerState(status: unknown) {
74 const raw = (status ?? null) as RemoteStatus | null;
75 const goal = typeof raw?.goal === "string" ? raw.goal.trim() : "";
76 const toolApprovalMode: ToolApprovalMode = normalizeToolApprovalMode(typeof raw?.toolApprovalMode === "string" ? raw.toolApprovalMode : undefined);
77 const rawGoalStatus = raw?.goalStatus;
78 const goalStatus: GoalStatus | undefined = rawGoalStatus === "running" || rawGoalStatus === "complete"
79 || rawGoalStatus === "blocked" || rawGoalStatus === "stopped" ? rawGoalStatus : undefined;
80 const effort = raw?.effort as Partial<EffortInfo> | undefined;
81 const qualityFloor: QualityFloor = raw?.qualityFloor === "delivery" ? "delivery" : "standard";
82 return {
83 modelLabel: typeof raw?.label === "string" ? raw.label : "",
84 composerProfile: {
85 collaborationMode: goal ? "goal" as const : raw?.plan === true ? "plan" as const : "normal" as const,
86 toolApprovalMode,
87 goal,
88 goalStatus,
89 qualityFloor,
90 },
91 effort: effort && typeof effort.supported === "boolean"
92 ? {
93 supported: effort.supported,
94 current: typeof effort.current === "string" ? effort.current : "auto",
95 default: typeof effort.default === "string" ? effort.default : "",
96 levels: Array.isArray(effort.levels) ? effort.levels.filter((level): level is string => typeof level === "string") : [],
97 }
98 : undefined,
99 };
100 }
101
102 export function remoteGoalRuntime(status: unknown): GoalRuntime | undefined {
103 const value = (status as RemoteStatus | null)?.goalRuntime;
104 if (!value || typeof value !== "object") return undefined;
105 const runtime = value as GoalRuntime;
106 return typeof runtime.turnsUsed === "number" && typeof runtime.tokensUsed === "number" ? runtime : undefined;
107 }
108
109 export function remoteCheckpoints(value: unknown): CheckpointMeta[] {
110 if (!Array.isArray(value)) return [];
111 return value.flatMap((entry) => {
112 const raw = (entry ?? null) as Record<string, unknown> | null;
113 if (!raw || typeof raw.turn !== "number" || !Number.isFinite(raw.turn)) return [];
114 const files = Array.isArray(raw.files)
115 ? raw.files.filter((path): path is string => typeof path === "string")
116 : [];
117 const numericFileCount = typeof raw.files === "number" ? raw.files : raw.fileCount;
118 const fileCount = typeof numericFileCount === "number" && Number.isFinite(numericFileCount)
119 ? Math.max(0, numericFileCount)
120 : files.length;
121 return [{
122 turn: raw.turn,
123 prompt: typeof raw.prompt === "string" ? raw.prompt : "",
124 files,
125 fileCount,
126 filesTruncated: raw.filesTruncated === true,
127 turnFileCount: typeof raw.turnFileCount === "number" ? raw.turnFileCount : undefined,
128 time: typeof raw.time === "number" ? raw.time : 0,
129 canCode: raw.canCode === true,
130 canConversation: raw.canConversation === true,
131 coverage: typeof raw.coverage === "string" ? raw.coverage : undefined,
132 coverageGaps: Array.isArray(raw.coverageGaps)
133 ? raw.coverageGaps.filter((gap): gap is string => typeof gap === "string")
134 : undefined,
135 expiredFilePayload: raw.expiredFilePayload === true,
136 activeWriters: typeof raw.activeWriters === "number" ? raw.activeWriters : undefined,
137 legacy: raw.legacy === true,
138 canUndoFiles: raw.canUndoFiles === true,
139 disabledReason: typeof raw.disabledReason === "string" ? raw.disabledReason : undefined,
140 }];
141 });
142 }
143
143 lines TYPESCRIPT