fix(types): Desktop type safety hardening (TYPE-01)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- Unify ConnectionState: kernel-types.ts now canonical source
  with 'handshaking', gateway-types.ts re-exports
- PromptTemplateInfo source/status → union literals
- PromptVariable.type → union literal
- CreateRoleRequest id/permissions → optional
- PropertyPanel: replace 13 as any with typed accessor pattern
- chatStore: window cast via as unknown as Record
This commit is contained in:
iven
2026-04-05 01:30:29 +08:00
parent 36168d6978
commit 3b0ab1a7b7
6 changed files with 32 additions and 26 deletions

View File

@@ -87,8 +87,13 @@ function renderTypeSpecificFields(
data: Partial<WorkflowNodeData>,
onChange: (field: string, value: unknown) => void
) {
// Type-safe property accessor for union-typed node data
// Type-safe property accessors for union-typed node data
const d = data as Record<string, unknown>;
const str = (key: string): string => (d[key] as string) || '';
const num = (key: string): number | string => (d[key] as number) ?? '';
const bool = (key: string): boolean => (d[key] as boolean) || false;
const arr = (key: string): string[] => (d[key] as string[]) || [];
const obj = (key: string): Record<string, unknown> => (d[key] as Record<string, unknown>) || {};
switch (type) {
case 'input':
return (
@@ -99,7 +104,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.variableName || ''}
value={str('variableName')}
onChange={(e) => onChange('variableName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/>
@@ -109,7 +114,7 @@ function renderTypeSpecificFields(
Default Value
</label>
<textarea
value={d.defaultValue || ''}
value={str('defaultValue')}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
@@ -134,7 +139,7 @@ function renderTypeSpecificFields(
Template
</label>
<textarea
value={d.template || ''}
value={str('template')}
onChange={(e) => onChange('template', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm"
rows={6}
@@ -146,7 +151,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.model || ''}
value={str('model')}
onChange={(e) => onChange('model', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="e.g., gpt-4"
@@ -161,7 +166,7 @@ function renderTypeSpecificFields(
min="0"
max="2"
step="0.1"
value={d.temperature ?? ''}
value={num('temperature')}
onChange={(e) => onChange('temperature', parseFloat(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
@@ -169,7 +174,7 @@ function renderTypeSpecificFields(
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={d.jsonMode || false}
checked={bool('jsonMode')}
onChange={(e) => onChange('jsonMode', e.target.checked)}
className="w-4 h-4 text-blue-600 rounded"
/>
@@ -187,7 +192,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.skillId || ''}
value={str('skillId')}
onChange={(e) => onChange('skillId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/>
@@ -197,7 +202,7 @@ function renderTypeSpecificFields(
Input Mappings (JSON)
</label>
<textarea
value={JSON.stringify(d.inputMappings || {}, null, 2)}
value={JSON.stringify(obj('inputMappings'), null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
@@ -222,7 +227,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.handId || ''}
value={str('handId')}
onChange={(e) => onChange('handId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/>
@@ -233,7 +238,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.action || ''}
value={str('action')}
onChange={(e) => onChange('action', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
@@ -253,13 +258,13 @@ function renderTypeSpecificFields(
<label key={format} className="flex items-center gap-2">
<input
type="checkbox"
checked={(d.formats || []).includes(format)}
checked={arr('formats').includes(format)}
onChange={(e) => {
const formats = d.formats || [];
const formats = arr('formats');
if (e.target.checked) {
onChange('formats', [...formats, format]);
} else {
onChange('formats', formats.filter((f: string) => f !== format));
onChange('formats', formats.filter((f) => f !== format));
}
}}
className="w-4 h-4 text-blue-600 rounded"
@@ -275,7 +280,7 @@ function renderTypeSpecificFields(
</label>
<input
type="text"
value={d.outputDir || ''}
value={str('outputDir')}
onChange={(e) => onChange('outputDir', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="./output"

View File

@@ -90,7 +90,7 @@ export interface ZclawStreamEvent {
}
// === Connection State ===
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
// Re-export from kernel-types to maintain single source of truth
export type { ConnectionState } from './kernel-types';
export type EventCallback = (payload: unknown) => void;

View File

@@ -7,7 +7,7 @@
// === Connection & Status Types ===
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
export interface KernelStatus {
initialized: boolean;

View File

@@ -188,9 +188,9 @@ export interface PromptTemplateInfo {
name: string;
category: string;
description: string | null;
source: string;
source: 'builtin' | 'custom';
current_version: number;
status: string;
status: 'active' | 'deprecated' | 'archived';
created_at: string;
updated_at: string;
}
@@ -211,7 +211,7 @@ export interface PromptVersionInfo {
/** Prompt variable definition */
export interface PromptVariable {
name: string;
type: string;
type: 'string' | 'number' | 'select' | 'boolean';
default_value?: string;
description?: string;
required?: boolean;
@@ -432,10 +432,10 @@ export interface RoleInfo {
/** Create role request */
export interface CreateRoleRequest {
id: string;
id?: string;
name: string;
description?: string;
permissions: string[];
permissions?: string[];
}
/** Update role request */

View File

@@ -354,7 +354,7 @@ if (import.meta.hot) {
// Dev-only: Expose stores to window for E2E testing
if (import.meta.env.DEV && typeof window !== 'undefined') {
const w = window as Record<string, unknown>;
const w = window as unknown as Record<string, unknown>;
w.__ZCLAW_STORES__ = (w.__ZCLAW_STORES__ as Record<string, unknown>) || {};
const stores = w.__ZCLAW_STORES__ as Record<string, unknown>;
stores.chat = useChatStore;