@pokit/tabs-core 0.0.1 → 0.0.2
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/package.json +7 -7
- package/src/constants/help-content.ts +49 -0
- package/src/constants/index.ts +10 -0
- package/src/constants/keyboard.ts +37 -0
- package/src/hooks/index.ts +20 -0
- package/src/hooks/use-keyboard-handler.ts +318 -0
- package/src/hooks/use-tabs-state.ts +150 -0
- package/src/process-manager.ts +235 -0
- package/src/ring-buffer.ts +338 -0
- package/src/state-reducer.ts +206 -0
- package/src/types.ts +106 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Reducer for Event-Driven CLI
|
|
3
|
+
*
|
|
4
|
+
* Builds a state tree from CLI events using a reducer pattern.
|
|
5
|
+
* Framework-agnostic - used by both Ink and OpenTUI adapters.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CLIEvent, ActivityId, GroupId } from '@pokit/core';
|
|
9
|
+
import type { EventDrivenState, ActivityNode, GroupNode } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create initial state
|
|
13
|
+
*/
|
|
14
|
+
export function createInitialState(): EventDrivenState {
|
|
15
|
+
return {
|
|
16
|
+
appName: undefined,
|
|
17
|
+
version: undefined,
|
|
18
|
+
exitCode: undefined,
|
|
19
|
+
activities: new Map(),
|
|
20
|
+
groups: new Map(),
|
|
21
|
+
rootChildren: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reducer function for CLI events
|
|
27
|
+
*/
|
|
28
|
+
export function reducer(state: EventDrivenState, event: CLIEvent): EventDrivenState {
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case 'root:start':
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
appName: event.appName,
|
|
34
|
+
version: event.version,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
case 'root:end':
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
exitCode: event.exitCode,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
case 'group:start': {
|
|
44
|
+
const newGroup: GroupNode = {
|
|
45
|
+
type: 'group',
|
|
46
|
+
id: event.id,
|
|
47
|
+
parentId: event.parentId,
|
|
48
|
+
label: event.label,
|
|
49
|
+
layout: event.layout,
|
|
50
|
+
children: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const newGroups = new Map(state.groups);
|
|
54
|
+
newGroups.set(event.id, newGroup);
|
|
55
|
+
|
|
56
|
+
if (event.parentId) {
|
|
57
|
+
const parentGroup = state.groups.get(event.parentId);
|
|
58
|
+
if (parentGroup) {
|
|
59
|
+
const updatedParent: GroupNode = {
|
|
60
|
+
...parentGroup,
|
|
61
|
+
children: [...parentGroup.children, event.id],
|
|
62
|
+
};
|
|
63
|
+
newGroups.set(event.parentId, updatedParent);
|
|
64
|
+
}
|
|
65
|
+
return { ...state, groups: newGroups };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
groups: newGroups,
|
|
71
|
+
rootChildren: [...state.rootChildren, event.id],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'group:end':
|
|
76
|
+
return state;
|
|
77
|
+
|
|
78
|
+
case 'activity:start': {
|
|
79
|
+
const newActivity: ActivityNode = {
|
|
80
|
+
type: 'activity',
|
|
81
|
+
id: event.id,
|
|
82
|
+
parentId: event.parentId,
|
|
83
|
+
label: event.label,
|
|
84
|
+
status: 'running',
|
|
85
|
+
meta: event.meta,
|
|
86
|
+
logs: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const newActivities = new Map(state.activities);
|
|
90
|
+
newActivities.set(event.id, newActivity);
|
|
91
|
+
|
|
92
|
+
if (event.parentId) {
|
|
93
|
+
const parentGroup = state.groups.get(event.parentId as GroupId);
|
|
94
|
+
if (parentGroup) {
|
|
95
|
+
const newGroups = new Map(state.groups);
|
|
96
|
+
const updatedParent: GroupNode = {
|
|
97
|
+
...parentGroup,
|
|
98
|
+
children: [...parentGroup.children, event.id],
|
|
99
|
+
};
|
|
100
|
+
newGroups.set(event.parentId as GroupId, updatedParent);
|
|
101
|
+
return { ...state, activities: newActivities, groups: newGroups };
|
|
102
|
+
}
|
|
103
|
+
return { ...state, activities: newActivities };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...state,
|
|
108
|
+
activities: newActivities,
|
|
109
|
+
rootChildren: [...state.rootChildren, event.id],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'activity:update': {
|
|
114
|
+
const activity = state.activities.get(event.id);
|
|
115
|
+
if (!activity) return state;
|
|
116
|
+
|
|
117
|
+
const updatedActivity: ActivityNode = {
|
|
118
|
+
...activity,
|
|
119
|
+
progress: event.payload.progress ?? activity.progress,
|
|
120
|
+
message: event.payload.message ?? activity.message,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const newActivities = new Map(state.activities);
|
|
124
|
+
newActivities.set(event.id, updatedActivity);
|
|
125
|
+
|
|
126
|
+
return { ...state, activities: newActivities };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case 'activity:success': {
|
|
130
|
+
const activity = state.activities.get(event.id);
|
|
131
|
+
if (!activity) return state;
|
|
132
|
+
|
|
133
|
+
const updatedActivity: ActivityNode = {
|
|
134
|
+
...activity,
|
|
135
|
+
status: 'success',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const newActivities = new Map(state.activities);
|
|
139
|
+
newActivities.set(event.id, updatedActivity);
|
|
140
|
+
|
|
141
|
+
return { ...state, activities: newActivities };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'activity:failure': {
|
|
145
|
+
const activity = state.activities.get(event.id);
|
|
146
|
+
if (!activity) return state;
|
|
147
|
+
|
|
148
|
+
const errorMessage = event.error instanceof Error ? event.error.message : String(event.error);
|
|
149
|
+
|
|
150
|
+
const updatedActivity: ActivityNode = {
|
|
151
|
+
...activity,
|
|
152
|
+
status: 'failure',
|
|
153
|
+
message: errorMessage,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const newActivities = new Map(state.activities);
|
|
157
|
+
newActivities.set(event.id, updatedActivity);
|
|
158
|
+
|
|
159
|
+
return { ...state, activities: newActivities };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'log': {
|
|
163
|
+
if (!event.activityId) return state;
|
|
164
|
+
|
|
165
|
+
const activity = state.activities.get(event.activityId);
|
|
166
|
+
if (!activity) return state;
|
|
167
|
+
|
|
168
|
+
const updatedActivity: ActivityNode = {
|
|
169
|
+
...activity,
|
|
170
|
+
logs: [...activity.logs, { level: event.level, message: event.message }],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const newActivities = new Map(state.activities);
|
|
174
|
+
newActivities.set(event.activityId, updatedActivity);
|
|
175
|
+
|
|
176
|
+
return { ...state, activities: newActivities };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get activities that belong to a tabs group
|
|
186
|
+
*/
|
|
187
|
+
export function getTabsGroupActivities(state: EventDrivenState, groupId: GroupId): ActivityNode[] {
|
|
188
|
+
const group = state.groups.get(groupId);
|
|
189
|
+
if (!group || group.layout !== 'tabs') return [];
|
|
190
|
+
|
|
191
|
+
return group.children
|
|
192
|
+
.map((childId) => state.activities.get(childId as ActivityId))
|
|
193
|
+
.filter((activity): activity is ActivityNode => activity !== undefined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find the first tabs group in the state
|
|
198
|
+
*/
|
|
199
|
+
export function findTabsGroup(state: EventDrivenState): GroupNode | undefined {
|
|
200
|
+
for (const group of state.groups.values()) {
|
|
201
|
+
if (group.layout === 'tabs') {
|
|
202
|
+
return group;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Types for CLI Tabs
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic types used by both Ink and OpenTUI adapters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ActivityId, GroupId, GroupLayout } from '@pokit/core';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Tab Process Types
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export type TabStatus = 'running' | 'done' | 'error' | 'stopped';
|
|
14
|
+
|
|
15
|
+
export type TabProcess = {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
exec: string;
|
|
19
|
+
output: string[];
|
|
20
|
+
status: TabStatus;
|
|
21
|
+
exitCode?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const MAX_OUTPUT_LINES = 10_000;
|
|
25
|
+
export const MAX_LINE_LENGTH = 5_000;
|
|
26
|
+
export const BUFFER_WARNING_THRESHOLD = 80; // Percentage
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Status Indicator Styling
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Status indicator configuration for tabs.
|
|
34
|
+
*
|
|
35
|
+
* Color scheme:
|
|
36
|
+
* - running: green (active process)
|
|
37
|
+
* - done: cyan (completed/inactive)
|
|
38
|
+
* - error: red (failed)
|
|
39
|
+
* - stopped: yellow (manually stopped)
|
|
40
|
+
*/
|
|
41
|
+
export type StatusIndicator = {
|
|
42
|
+
color: string;
|
|
43
|
+
colorBright: string;
|
|
44
|
+
icon: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const STATUS_INDICATORS: Record<TabStatus, StatusIndicator> = {
|
|
48
|
+
running: { color: '#00AA00', colorBright: '#00FF00', icon: '●' },
|
|
49
|
+
done: { color: '#00AAAA', colorBright: '#00FFFF', icon: '○' },
|
|
50
|
+
error: { color: '#AA0000', colorBright: '#FF0000', icon: '✗' },
|
|
51
|
+
stopped: { color: '#AAAA00', colorBright: '#FFFF00', icon: '■' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function getStatusIndicator(
|
|
55
|
+
status: TabStatus,
|
|
56
|
+
bright: boolean = false
|
|
57
|
+
): { color: string; icon: string } {
|
|
58
|
+
const indicator = STATUS_INDICATORS[status];
|
|
59
|
+
return {
|
|
60
|
+
color: bright ? indicator.colorBright : indicator.color,
|
|
61
|
+
icon: indicator.icon,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Event-Driven State Types
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Activity node in the state tree
|
|
71
|
+
*/
|
|
72
|
+
export type ActivityNode = {
|
|
73
|
+
type: 'activity';
|
|
74
|
+
id: ActivityId;
|
|
75
|
+
parentId?: GroupId | ActivityId;
|
|
76
|
+
label: string;
|
|
77
|
+
status: 'running' | 'success' | 'failure';
|
|
78
|
+
progress?: number;
|
|
79
|
+
message?: string;
|
|
80
|
+
meta?: Record<string, unknown>;
|
|
81
|
+
logs: Array<{ level: string; message: string }>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Group node in the state tree
|
|
86
|
+
*/
|
|
87
|
+
export type GroupNode = {
|
|
88
|
+
type: 'group';
|
|
89
|
+
id: GroupId;
|
|
90
|
+
parentId?: GroupId;
|
|
91
|
+
label: string;
|
|
92
|
+
layout: GroupLayout;
|
|
93
|
+
children: Array<ActivityId | GroupId>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Root state for the event-driven CLI
|
|
98
|
+
*/
|
|
99
|
+
export type EventDrivenState = {
|
|
100
|
+
appName?: string;
|
|
101
|
+
version?: string;
|
|
102
|
+
exitCode?: number;
|
|
103
|
+
activities: Map<ActivityId, ActivityNode>;
|
|
104
|
+
groups: Map<GroupId, GroupNode>;
|
|
105
|
+
rootChildren: Array<ActivityId | GroupId>;
|
|
106
|
+
};
|