@pokit/reporter-web 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/adapter.d.ts +37 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +53 -0
- package/dist/components/CommandBlock.d.ts +35 -0
- package/dist/components/CommandBlock.d.ts.map +1 -0
- package/dist/components/CommandBlock.js +15 -0
- package/dist/components/ContentBox.d.ts +23 -0
- package/dist/components/ContentBox.d.ts.map +1 -0
- package/dist/components/ContentBox.js +4 -0
- package/dist/components/FilePreview.d.ts +38 -0
- package/dist/components/FilePreview.d.ts.map +1 -0
- package/dist/components/FilePreview.js +15 -0
- package/dist/components/ProgressIndicator.d.ts +21 -0
- package/dist/components/ProgressIndicator.d.ts.map +1 -0
- package/dist/components/ProgressIndicator.js +7 -0
- package/dist/components/TutorialStep.d.ts +28 -0
- package/dist/components/TutorialStep.d.ts.map +1 -0
- package/dist/components/TutorialStep.js +4 -0
- package/dist/components/index.d.ts +30 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +29 -0
- package/dist/hooks.d.ts +94 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +201 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +345 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +58 -0
- package/src/index.ts +59 -0
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporter Web React Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for subscribing to reporter state using useSyncExternalStore.
|
|
5
|
+
* Provides full state subscription and selective subscriptions for individual
|
|
6
|
+
* activities and groups.
|
|
7
|
+
*/
|
|
8
|
+
import { useSyncExternalStore, useCallback, useRef } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to the full reporter state
|
|
11
|
+
*
|
|
12
|
+
* @param store - The reporter store
|
|
13
|
+
* @returns Current reporter state
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const state = useReporterState(store);
|
|
18
|
+
* return <div>Status: {state.root.status}</div>;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useReporterState(store) {
|
|
22
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to a single activity by ID
|
|
26
|
+
* Returns undefined if activity doesn't exist
|
|
27
|
+
*
|
|
28
|
+
* @param store - The reporter store
|
|
29
|
+
* @param id - Activity ID to subscribe to
|
|
30
|
+
* @returns Activity state or undefined
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const activity = useActivity(store, 'task-1');
|
|
35
|
+
* if (!activity) return null;
|
|
36
|
+
* return <div>{activity.label}: {activity.status}</div>;
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useActivity(store, id) {
|
|
40
|
+
// Track the previous activity reference for shallow comparison
|
|
41
|
+
const prevActivityRef = useRef(undefined);
|
|
42
|
+
const getSnapshot = useCallback(() => {
|
|
43
|
+
const state = store.getSnapshot();
|
|
44
|
+
const activity = state.activities.get(id);
|
|
45
|
+
// Return same reference if activity hasn't changed (shallow comparison)
|
|
46
|
+
if (prevActivityRef.current === activity) {
|
|
47
|
+
return prevActivityRef.current;
|
|
48
|
+
}
|
|
49
|
+
// Check if activity content is the same (for Map recreation scenarios)
|
|
50
|
+
if (prevActivityRef.current &&
|
|
51
|
+
activity &&
|
|
52
|
+
prevActivityRef.current.id === activity.id &&
|
|
53
|
+
prevActivityRef.current.status === activity.status &&
|
|
54
|
+
prevActivityRef.current.progress === activity.progress &&
|
|
55
|
+
prevActivityRef.current.message === activity.message &&
|
|
56
|
+
prevActivityRef.current.justStarted === activity.justStarted &&
|
|
57
|
+
prevActivityRef.current.justCompleted === activity.justCompleted &&
|
|
58
|
+
prevActivityRef.current.justFailed === activity.justFailed &&
|
|
59
|
+
prevActivityRef.current.completedAt === activity.completedAt) {
|
|
60
|
+
return prevActivityRef.current;
|
|
61
|
+
}
|
|
62
|
+
prevActivityRef.current = activity;
|
|
63
|
+
return activity;
|
|
64
|
+
}, [store, id]);
|
|
65
|
+
const getServerSnapshot = useCallback(() => {
|
|
66
|
+
return store.getServerSnapshot().activities.get(id);
|
|
67
|
+
}, [store, id]);
|
|
68
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to a single group by ID
|
|
72
|
+
* Returns undefined if group doesn't exist
|
|
73
|
+
*
|
|
74
|
+
* @param store - The reporter store
|
|
75
|
+
* @param id - Group ID to subscribe to
|
|
76
|
+
* @returns Group state or undefined
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* const group = useGroup(store, 'checks');
|
|
81
|
+
* if (!group) return null;
|
|
82
|
+
* return <div>{group.label} ({group.activityIds.length} tasks)</div>;
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function useGroup(store, id) {
|
|
86
|
+
// Track the previous group reference for shallow comparison
|
|
87
|
+
const prevGroupRef = useRef(undefined);
|
|
88
|
+
const getSnapshot = useCallback(() => {
|
|
89
|
+
const state = store.getSnapshot();
|
|
90
|
+
const group = state.groups.get(id);
|
|
91
|
+
// Return same reference if group hasn't changed
|
|
92
|
+
if (prevGroupRef.current === group) {
|
|
93
|
+
return prevGroupRef.current;
|
|
94
|
+
}
|
|
95
|
+
// Check if group content is the same (for Map recreation scenarios)
|
|
96
|
+
if (prevGroupRef.current &&
|
|
97
|
+
group &&
|
|
98
|
+
prevGroupRef.current.id === group.id &&
|
|
99
|
+
prevGroupRef.current.label === group.label &&
|
|
100
|
+
prevGroupRef.current.hasFailure === group.hasFailure &&
|
|
101
|
+
prevGroupRef.current.justStarted_group === group.justStarted_group &&
|
|
102
|
+
prevGroupRef.current.justEnded === group.justEnded &&
|
|
103
|
+
prevGroupRef.current.endedAt === group.endedAt &&
|
|
104
|
+
prevGroupRef.current.activityIds.length === group.activityIds.length &&
|
|
105
|
+
prevGroupRef.current.childGroupIds.length === group.childGroupIds.length) {
|
|
106
|
+
return prevGroupRef.current;
|
|
107
|
+
}
|
|
108
|
+
prevGroupRef.current = group;
|
|
109
|
+
return group;
|
|
110
|
+
}, [store, id]);
|
|
111
|
+
const getServerSnapshot = useCallback(() => {
|
|
112
|
+
return store.getServerSnapshot().groups.get(id);
|
|
113
|
+
}, [store, id]);
|
|
114
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Subscribe to root state only
|
|
118
|
+
* More efficient than useReporterState when you only need root info
|
|
119
|
+
*
|
|
120
|
+
* @param store - The reporter store
|
|
121
|
+
* @returns Root state
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx
|
|
125
|
+
* const root = useRootState(store);
|
|
126
|
+
* return <div>App: {root.appName} - {root.status}</div>;
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export function useRootState(store) {
|
|
130
|
+
const prevRootRef = useRef(undefined);
|
|
131
|
+
const getSnapshot = useCallback(() => {
|
|
132
|
+
const state = store.getSnapshot();
|
|
133
|
+
const root = state.root;
|
|
134
|
+
// Return same reference if root hasn't changed
|
|
135
|
+
if (prevRootRef.current &&
|
|
136
|
+
prevRootRef.current.status === root.status &&
|
|
137
|
+
prevRootRef.current.appName === root.appName &&
|
|
138
|
+
prevRootRef.current.exitCode === root.exitCode) {
|
|
139
|
+
return prevRootRef.current;
|
|
140
|
+
}
|
|
141
|
+
prevRootRef.current = root;
|
|
142
|
+
return root;
|
|
143
|
+
}, [store]);
|
|
144
|
+
const getServerSnapshot = useCallback(() => {
|
|
145
|
+
return store.getServerSnapshot().root;
|
|
146
|
+
}, [store]);
|
|
147
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Subscribe to logs
|
|
151
|
+
* Returns the full log array
|
|
152
|
+
*
|
|
153
|
+
* @param store - The reporter store
|
|
154
|
+
* @returns Array of log entries
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* const logs = useLogs(store);
|
|
159
|
+
* return (
|
|
160
|
+
* <ul>
|
|
161
|
+
* {logs.map(log => <li key={log.id}>{log.message}</li>)}
|
|
162
|
+
* </ul>
|
|
163
|
+
* );
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function useLogs(store) {
|
|
167
|
+
const prevLogsRef = useRef(undefined);
|
|
168
|
+
const getSnapshot = useCallback(() => {
|
|
169
|
+
const state = store.getSnapshot();
|
|
170
|
+
const logs = state.logs;
|
|
171
|
+
// Return same reference if logs haven't changed
|
|
172
|
+
if (prevLogsRef.current && prevLogsRef.current.length === logs.length) {
|
|
173
|
+
// Quick check - if lengths match and last item is same, assume unchanged
|
|
174
|
+
if (logs.length === 0 ||
|
|
175
|
+
prevLogsRef.current[logs.length - 1]?.id === logs[logs.length - 1]?.id) {
|
|
176
|
+
return prevLogsRef.current;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
prevLogsRef.current = logs;
|
|
180
|
+
return logs;
|
|
181
|
+
}, [store]);
|
|
182
|
+
const getServerSnapshot = useCallback(() => {
|
|
183
|
+
return store.getServerSnapshot().logs;
|
|
184
|
+
}, [store]);
|
|
185
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Subscribe to suspended state
|
|
189
|
+
*
|
|
190
|
+
* @param store - The reporter store
|
|
191
|
+
* @returns Whether reporter is suspended
|
|
192
|
+
*/
|
|
193
|
+
export function useSuspended(store) {
|
|
194
|
+
const getSnapshot = useCallback(() => {
|
|
195
|
+
return store.getSnapshot().suspended;
|
|
196
|
+
}, [store]);
|
|
197
|
+
const getServerSnapshot = useCallback(() => {
|
|
198
|
+
return store.getServerSnapshot().suspended;
|
|
199
|
+
}, [store]);
|
|
200
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
201
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pokit/reporter-web
|
|
3
|
+
*
|
|
4
|
+
* Web/React implementation of the ReporterAdapter interface.
|
|
5
|
+
* Provides a store for state management and React hooks for subscription.
|
|
6
|
+
*/
|
|
7
|
+
export { createReporterStore } from './store';
|
|
8
|
+
export type { CreateReporterStoreOptions, ReporterStoreWithHandler } from './store';
|
|
9
|
+
export { createWebReporterAdapter } from './adapter';
|
|
10
|
+
export { useReporterState, useActivity, useGroup, useRootState, useLogs, useSuspended, } from './hooks';
|
|
11
|
+
export type { RootStatus, ActivityStatus, TemporalMarkers, ActivityState, GroupState, LogEntry, RootState, ReporterState, StateListener, ReporterStore, } from './types';
|
|
12
|
+
export { TutorialStep, FilePreview, CommandBlock, ProgressIndicator, ContentBox, } from './components';
|
|
13
|
+
export type { TutorialStepProps, TutorialStepStatus, FilePreviewProps, FilePreviewStatus, FilePreviewActionProps, CommandBlockProps, CommandBlockStatus, CommandBlockActionProps, ProgressIndicatorProps, ContentBoxProps, ContentBoxVariant, } from './components';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,YAAY,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAGrD,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,YAAY,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,aAAa,EACb,UAAU,EACV,QAAQ,EACR,SAAS,EACT,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pokit/reporter-web
|
|
3
|
+
*
|
|
4
|
+
* Web/React implementation of the ReporterAdapter interface.
|
|
5
|
+
* Provides a store for state management and React hooks for subscription.
|
|
6
|
+
*/
|
|
7
|
+
// Store
|
|
8
|
+
export { createReporterStore } from './store';
|
|
9
|
+
// Adapter
|
|
10
|
+
export { createWebReporterAdapter } from './adapter';
|
|
11
|
+
// Hooks
|
|
12
|
+
export { useReporterState, useActivity, useGroup, useRootState, useLogs, useSuspended, } from './hooks';
|
|
13
|
+
// Components
|
|
14
|
+
export { TutorialStep, FilePreview, CommandBlock, ProgressIndicator, ContentBox, } from './components';
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporter Web Store
|
|
3
|
+
*
|
|
4
|
+
* Creates an external store for React integration.
|
|
5
|
+
* Handles all CLIEvent types and maintains normalized state.
|
|
6
|
+
*/
|
|
7
|
+
import type { CLIEvent } from '@pokit/core';
|
|
8
|
+
import type { ReporterStore } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Options for creating a reporter store
|
|
11
|
+
*/
|
|
12
|
+
export type CreateReporterStoreOptions = {
|
|
13
|
+
/** Custom delay for clearing temporal markers (default: 600ms) */
|
|
14
|
+
temporalMarkerDelay?: number;
|
|
15
|
+
/** Disable temporal marker auto-clearing (useful for testing) */
|
|
16
|
+
disableTemporalMarkerClearing?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Type for the store with internal event handler exposed
|
|
20
|
+
*/
|
|
21
|
+
export type ReporterStoreWithHandler = ReporterStore & {
|
|
22
|
+
_handleEvent: (event: CLIEvent) => void;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Create a reporter store for React integration
|
|
26
|
+
*
|
|
27
|
+
* @param options - Optional configuration
|
|
28
|
+
* @returns Store compatible with useSyncExternalStore
|
|
29
|
+
*/
|
|
30
|
+
export declare function createReporterStore(options?: CreateReporterStoreOptions): ReporterStoreWithHandler;
|
|
31
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAuB,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAEV,aAAa,EAKd,MAAM,SAAS,CAAC;AAuBjB;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iEAAiE;IACjE,6BAA6B,CAAC,EAAE,OAAO,CAAC;CACzC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,aAAa,GAAG;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,wBAAwB,CAgWlG"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporter Web Store
|
|
3
|
+
*
|
|
4
|
+
* Creates an external store for React integration.
|
|
5
|
+
* Handles all CLIEvent types and maintains normalized state.
|
|
6
|
+
*/
|
|
7
|
+
/** Default delay for clearing temporal markers (ms) */
|
|
8
|
+
const TEMPORAL_MARKER_DELAY = 600;
|
|
9
|
+
/** Counter for generating unique log IDs */
|
|
10
|
+
let logIdCounter = 0;
|
|
11
|
+
/**
|
|
12
|
+
* Create initial reporter state
|
|
13
|
+
*/
|
|
14
|
+
function createInitialState() {
|
|
15
|
+
return {
|
|
16
|
+
root: {
|
|
17
|
+
status: 'idle',
|
|
18
|
+
},
|
|
19
|
+
groups: new Map(),
|
|
20
|
+
activities: new Map(),
|
|
21
|
+
logs: [],
|
|
22
|
+
suspended: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a reporter store for React integration
|
|
27
|
+
*
|
|
28
|
+
* @param options - Optional configuration
|
|
29
|
+
* @returns Store compatible with useSyncExternalStore
|
|
30
|
+
*/
|
|
31
|
+
export function createReporterStore(options) {
|
|
32
|
+
const temporalMarkerDelay = options?.temporalMarkerDelay ?? TEMPORAL_MARKER_DELAY;
|
|
33
|
+
const disableTemporalMarkerClearing = options?.disableTemporalMarkerClearing ?? false;
|
|
34
|
+
let state = createInitialState();
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
/**
|
|
37
|
+
* Notify all listeners of state change
|
|
38
|
+
*/
|
|
39
|
+
function notifyListeners() {
|
|
40
|
+
for (const listener of listeners) {
|
|
41
|
+
listener();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Schedule clearing of temporal markers for an activity
|
|
46
|
+
*/
|
|
47
|
+
function scheduleActivityMarkerClear(activityId, markers) {
|
|
48
|
+
if (disableTemporalMarkerClearing)
|
|
49
|
+
return;
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
const activity = state.activities.get(activityId);
|
|
52
|
+
if (!activity)
|
|
53
|
+
return;
|
|
54
|
+
// Check if any markers are still set
|
|
55
|
+
const hasMarkers = markers.some((marker) => activity[marker]);
|
|
56
|
+
if (!hasMarkers)
|
|
57
|
+
return;
|
|
58
|
+
// Create new state with cleared markers
|
|
59
|
+
const updatedActivity = { ...activity };
|
|
60
|
+
for (const marker of markers) {
|
|
61
|
+
delete updatedActivity[marker];
|
|
62
|
+
}
|
|
63
|
+
const newActivities = new Map(state.activities);
|
|
64
|
+
newActivities.set(activityId, updatedActivity);
|
|
65
|
+
state = {
|
|
66
|
+
...state,
|
|
67
|
+
activities: newActivities,
|
|
68
|
+
};
|
|
69
|
+
notifyListeners();
|
|
70
|
+
}, temporalMarkerDelay);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Schedule clearing of temporal markers for a group
|
|
74
|
+
*/
|
|
75
|
+
function scheduleGroupMarkerClear(groupId, markers) {
|
|
76
|
+
if (disableTemporalMarkerClearing)
|
|
77
|
+
return;
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
const group = state.groups.get(groupId);
|
|
80
|
+
if (!group)
|
|
81
|
+
return;
|
|
82
|
+
// Check if any markers are still set
|
|
83
|
+
const hasMarkers = markers.some((marker) => group[marker]);
|
|
84
|
+
if (!hasMarkers)
|
|
85
|
+
return;
|
|
86
|
+
// Create new state with cleared markers
|
|
87
|
+
const updatedGroup = { ...group };
|
|
88
|
+
for (const marker of markers) {
|
|
89
|
+
delete updatedGroup[marker];
|
|
90
|
+
}
|
|
91
|
+
const newGroups = new Map(state.groups);
|
|
92
|
+
newGroups.set(groupId, updatedGroup);
|
|
93
|
+
state = {
|
|
94
|
+
...state,
|
|
95
|
+
groups: newGroups,
|
|
96
|
+
};
|
|
97
|
+
notifyListeners();
|
|
98
|
+
}, temporalMarkerDelay);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Handle a CLI event and update state
|
|
102
|
+
*/
|
|
103
|
+
function handleEvent(event) {
|
|
104
|
+
switch (event.type) {
|
|
105
|
+
case 'root:start': {
|
|
106
|
+
state = {
|
|
107
|
+
...state,
|
|
108
|
+
root: {
|
|
109
|
+
status: 'running',
|
|
110
|
+
appName: event.appName,
|
|
111
|
+
version: event.version,
|
|
112
|
+
startedAt: Date.now(),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
notifyListeners();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'root:end': {
|
|
119
|
+
state = {
|
|
120
|
+
...state,
|
|
121
|
+
root: {
|
|
122
|
+
...state.root,
|
|
123
|
+
status: event.exitCode === 0 ? 'complete' : 'error',
|
|
124
|
+
exitCode: event.exitCode,
|
|
125
|
+
endedAt: Date.now(),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
notifyListeners();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'group:start': {
|
|
132
|
+
const newGroup = {
|
|
133
|
+
id: event.id,
|
|
134
|
+
parentId: event.parentId,
|
|
135
|
+
label: event.label,
|
|
136
|
+
layout: event.layout,
|
|
137
|
+
activityIds: [],
|
|
138
|
+
childGroupIds: [],
|
|
139
|
+
hasFailure: false,
|
|
140
|
+
startedAt: Date.now(),
|
|
141
|
+
justStarted_group: true,
|
|
142
|
+
};
|
|
143
|
+
const newGroups = new Map(state.groups);
|
|
144
|
+
newGroups.set(event.id, newGroup);
|
|
145
|
+
// Add to parent's childGroupIds if parent exists
|
|
146
|
+
if (event.parentId) {
|
|
147
|
+
const parent = state.groups.get(event.parentId);
|
|
148
|
+
if (parent) {
|
|
149
|
+
newGroups.set(event.parentId, {
|
|
150
|
+
...parent,
|
|
151
|
+
childGroupIds: [...parent.childGroupIds, event.id],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
state = {
|
|
156
|
+
...state,
|
|
157
|
+
groups: newGroups,
|
|
158
|
+
};
|
|
159
|
+
notifyListeners();
|
|
160
|
+
scheduleGroupMarkerClear(event.id, ['justStarted_group']);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case 'group:end': {
|
|
164
|
+
const group = state.groups.get(event.id);
|
|
165
|
+
if (!group)
|
|
166
|
+
break;
|
|
167
|
+
const newGroups = new Map(state.groups);
|
|
168
|
+
newGroups.set(event.id, {
|
|
169
|
+
...group,
|
|
170
|
+
endedAt: Date.now(),
|
|
171
|
+
justEnded: true,
|
|
172
|
+
});
|
|
173
|
+
state = {
|
|
174
|
+
...state,
|
|
175
|
+
groups: newGroups,
|
|
176
|
+
};
|
|
177
|
+
notifyListeners();
|
|
178
|
+
scheduleGroupMarkerClear(event.id, ['justEnded']);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'activity:start': {
|
|
182
|
+
const newActivity = {
|
|
183
|
+
id: event.id,
|
|
184
|
+
parentId: event.parentId,
|
|
185
|
+
label: event.label,
|
|
186
|
+
status: 'running',
|
|
187
|
+
meta: event.meta,
|
|
188
|
+
startedAt: Date.now(),
|
|
189
|
+
justStarted: true,
|
|
190
|
+
};
|
|
191
|
+
const newActivities = new Map(state.activities);
|
|
192
|
+
newActivities.set(event.id, newActivity);
|
|
193
|
+
// Add to parent group's activityIds if parent is a group
|
|
194
|
+
const newGroups = new Map(state.groups);
|
|
195
|
+
if (event.parentId) {
|
|
196
|
+
const parentGroup = state.groups.get(event.parentId);
|
|
197
|
+
if (parentGroup) {
|
|
198
|
+
newGroups.set(event.parentId, {
|
|
199
|
+
...parentGroup,
|
|
200
|
+
activityIds: [...parentGroup.activityIds, event.id],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
state = {
|
|
205
|
+
...state,
|
|
206
|
+
activities: newActivities,
|
|
207
|
+
groups: newGroups,
|
|
208
|
+
};
|
|
209
|
+
notifyListeners();
|
|
210
|
+
scheduleActivityMarkerClear(event.id, ['justStarted']);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'activity:update': {
|
|
214
|
+
const activity = state.activities.get(event.id);
|
|
215
|
+
if (!activity)
|
|
216
|
+
break;
|
|
217
|
+
const { progress, message, ...rest } = event.payload;
|
|
218
|
+
const newActivities = new Map(state.activities);
|
|
219
|
+
newActivities.set(event.id, {
|
|
220
|
+
...activity,
|
|
221
|
+
progress: progress ?? activity.progress,
|
|
222
|
+
message: message ?? activity.message,
|
|
223
|
+
payload: {
|
|
224
|
+
...activity.payload,
|
|
225
|
+
...rest,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
state = {
|
|
229
|
+
...state,
|
|
230
|
+
activities: newActivities,
|
|
231
|
+
};
|
|
232
|
+
notifyListeners();
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'activity:success': {
|
|
236
|
+
const activity = state.activities.get(event.id);
|
|
237
|
+
if (!activity)
|
|
238
|
+
break;
|
|
239
|
+
const newActivities = new Map(state.activities);
|
|
240
|
+
newActivities.set(event.id, {
|
|
241
|
+
...activity,
|
|
242
|
+
status: 'success',
|
|
243
|
+
completedAt: Date.now(),
|
|
244
|
+
justCompleted: true,
|
|
245
|
+
});
|
|
246
|
+
state = {
|
|
247
|
+
...state,
|
|
248
|
+
activities: newActivities,
|
|
249
|
+
};
|
|
250
|
+
notifyListeners();
|
|
251
|
+
scheduleActivityMarkerClear(event.id, ['justCompleted']);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'activity:failure': {
|
|
255
|
+
const activity = state.activities.get(event.id);
|
|
256
|
+
if (!activity)
|
|
257
|
+
break;
|
|
258
|
+
const errorMessage = event.error instanceof Error ? event.error.message : String(event.error);
|
|
259
|
+
const newActivities = new Map(state.activities);
|
|
260
|
+
newActivities.set(event.id, {
|
|
261
|
+
...activity,
|
|
262
|
+
status: 'failure',
|
|
263
|
+
completedAt: Date.now(),
|
|
264
|
+
justFailed: true,
|
|
265
|
+
error: {
|
|
266
|
+
message: errorMessage,
|
|
267
|
+
remediation: event.remediation,
|
|
268
|
+
documentationUrl: event.documentationUrl,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// Mark parent group as having failure
|
|
272
|
+
const newGroups = new Map(state.groups);
|
|
273
|
+
if (activity.parentId) {
|
|
274
|
+
const parentGroup = state.groups.get(activity.parentId);
|
|
275
|
+
if (parentGroup) {
|
|
276
|
+
newGroups.set(activity.parentId, {
|
|
277
|
+
...parentGroup,
|
|
278
|
+
hasFailure: true,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
state = {
|
|
283
|
+
...state,
|
|
284
|
+
activities: newActivities,
|
|
285
|
+
groups: newGroups,
|
|
286
|
+
};
|
|
287
|
+
notifyListeners();
|
|
288
|
+
scheduleActivityMarkerClear(event.id, ['justFailed']);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'log': {
|
|
292
|
+
const logEntry = {
|
|
293
|
+
id: `log-${++logIdCounter}`,
|
|
294
|
+
activityId: event.activityId,
|
|
295
|
+
level: event.level,
|
|
296
|
+
message: event.message,
|
|
297
|
+
timestamp: Date.now(),
|
|
298
|
+
};
|
|
299
|
+
state = {
|
|
300
|
+
...state,
|
|
301
|
+
logs: [...state.logs, logEntry],
|
|
302
|
+
};
|
|
303
|
+
notifyListeners();
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case 'reporter:suspend': {
|
|
307
|
+
state = {
|
|
308
|
+
...state,
|
|
309
|
+
suspended: true,
|
|
310
|
+
};
|
|
311
|
+
notifyListeners();
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case 'reporter:resume': {
|
|
315
|
+
state = {
|
|
316
|
+
...state,
|
|
317
|
+
suspended: false,
|
|
318
|
+
};
|
|
319
|
+
notifyListeners();
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
getState() {
|
|
326
|
+
return state;
|
|
327
|
+
},
|
|
328
|
+
getSnapshot() {
|
|
329
|
+
return state;
|
|
330
|
+
},
|
|
331
|
+
getServerSnapshot() {
|
|
332
|
+
return state;
|
|
333
|
+
},
|
|
334
|
+
subscribe(listener) {
|
|
335
|
+
listeners.add(listener);
|
|
336
|
+
return () => {
|
|
337
|
+
listeners.delete(listener);
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
/**
|
|
341
|
+
* Internal method to handle events - exposed for adapter use
|
|
342
|
+
*/
|
|
343
|
+
_handleEvent: handleEvent,
|
|
344
|
+
};
|
|
345
|
+
}
|