Skip to content

Commit fa8dbea

Browse files
authored
Merge pull request #4 from mcode-webui/fix/webui-typecheck-gates-clean
fix(webui): 让 webui 的类型检查与仓库其余部分用同一套标准
2 parents 83321d0 + 7805d2b commit fa8dbea

6 files changed

Lines changed: 504 additions & 28 deletions

File tree

‎packages/webui/src/client/projection/workspace-progress.ts‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,20 @@ const SUBAGENT_REPORTED_EVENT_TYPES = [
215215
] as const;
216216
const TODO_EVENT_TYPES = ["todo_updated"] as const;
217217

218+
/**
219+
* `Array.prototype.includes` accepts any value, but these literal tuples are what
220+
* make a dispatch branch exhaustive, so membership has to narrow `type` instead of
221+
* merely comparing it — `type && TODO_EVENT_TYPES.includes(type)` still hands a
222+
* plain `string` to a `"todo_updated"` parameter. A frame carrying no event name
223+
* belongs to no branch, which is what `includes(undefined)` already answered.
224+
*/
225+
function isEventTypeOf<T extends string>(
226+
type: string | undefined,
227+
allowed: readonly T[],
228+
): type is T {
229+
return type !== undefined && (allowed as readonly string[]).includes(type);
230+
}
231+
218232
function terminalSubagentStatus(
219233
type: string,
220234
data: Record<string, unknown>,
@@ -231,11 +245,11 @@ export function reduceWebuiWorkspaceProgressEvent(
231245
sessionId?: string,
232246
): WebuiWorkspaceProgressState {
233247
const type = eventType(value);
234-
if (TODO_EVENT_TYPES.includes(type)) {
248+
if (isEventTypeOf(type, TODO_EVENT_TYPES)) {
235249
const todos = normalizeTodos(value.todos ?? eventData(value).todos);
236250
return todos ? { ...state, todos, hasTodoSnapshot: true } : state;
237251
}
238-
if (SUBAGENT_SPAWN_EVENT_TYPES.includes(type)) {
252+
if (isEventTypeOf(type, SUBAGENT_SPAWN_EVENT_TYPES)) {
239253
const subagent = subagentFromEvent(value, sessionId);
240254
return subagent
241255
? {
@@ -245,7 +259,7 @@ export function reduceWebuiWorkspaceProgressEvent(
245259
}
246260
: state;
247261
}
248-
if (SUBAGENT_STATUS_EVENT_TYPES.includes(type)) {
262+
if (isEventTypeOf(type, SUBAGENT_STATUS_EVENT_TYPES)) {
249263
const data = eventData(value);
250264
const childSessionId = stringValue(data, ["sessionId", "session_id"]);
251265
if (!childSessionId) return state;
@@ -262,7 +276,7 @@ export function reduceWebuiWorkspaceProgressEvent(
262276
hasSubagentSnapshot: true,
263277
};
264278
}
265-
if (SUBAGENT_REPORTED_EVENT_TYPES.includes(type)) {
279+
if (isEventTypeOf(type, SUBAGENT_REPORTED_EVENT_TYPES)) {
266280
const data = eventData(value);
267281
const childSessionId = stringValue(data, ["sessionId", "session_id"]);
268282
const existing = childSessionId
@@ -329,8 +343,12 @@ function applyTodoToolCalls(
329343
message: WebuiClientMessage | Record<string, unknown>,
330344
): WebuiWorkspaceProgressState {
331345
const loose = message as Record<string, unknown>;
346+
// `message` is a union, and its `Record<string, unknown>` arm types `toolCalls`
347+
// as `unknown` — so `message.toolCalls ?? []` leaves a non-iterable on the
348+
// right-hand side. Read the declared field through the same `Array.isArray`
349+
// guard as the snake_case one below it.
332350
const calls = [
333-
...(message.toolCalls ?? []),
351+
...(Array.isArray(message.toolCalls) ? message.toolCalls : []),
334352
...(Array.isArray(loose.tool_calls) ? loose.tool_calls : []),
335353
];
336354
let next = state;

‎packages/webui/tsconfig.json‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
{
2-
"extends": "../../tsconfig.node.json",
2+
"extends": "./tsconfig.paths.json",
33
"compilerOptions": {
44
"composite": false,
5-
"rootDir": "."
5+
// The path map resolves workspace packages to their TypeScript source, so the
6+
// program legitimately spans the repository rather than just this package.
7+
"rootDir": "../..",
8+
// `tsconfig.node.json` turns these on, but the workspace packages and the
9+
// vendored `third_party` sources this program pulls in are held to `strict`
10+
// without them — `tsconfig.standalone.json`, `pi-mono/tsconfig.base.json` and
11+
// `sandbox-runtime/tsconfig.json` all leave them off. TypeScript has no
12+
// per-directory options, so the program-wide value has to match the standard
13+
// its own inputs are actually written to.
14+
"noUncheckedIndexedAccess": false,
15+
"noImplicitOverride": false,
16+
// `third_party/pi-mono`'s TUI source uses the ES2024 `v` regular-expression
17+
// flag, which the inherited ES2022 target rejects.
18+
"target": "ES2024",
19+
// The path map points at `.ts` entry points, which TypeScript only accepts when
20+
// emitting is off. Every leaf config in this chain sets `noEmit`.
21+
"allowImportingTsExtensions": true
622
},
23+
// `third_party/pi-mono` reaches `highlight.js` through a deep subpath that
24+
// bypasses the package's own `types`, and ships an ambient declaration for it.
25+
// Ambient declarations only take effect when the file is part of the program, and
26+
// `extends` replaces `include` wholesale, so it cannot ride along there. `files`
27+
// is inherited instead, and unions with each leaf's own `include`.
28+
"files": [
29+
"../../third_party/pi-mono/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts"
30+
],
731
"include": ["src/**/*.ts", "src/**/*.tsx"]
832
}

0 commit comments

Comments
 (0)