@pokit/tabs-ink 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniel Grant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @pokit/tabs-ink
2
+
3
+ Tabbed terminal UI adapter for pok using [Ink](https://github.com/vadimdemedes/ink).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @pokit/tabs-ink
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { run } from '@pokit/core';
15
+ import { createTabsAdapter } from '@pokit/tabs-ink';
16
+
17
+ await run(args, {
18
+ tabs: createTabsAdapter(),
19
+ // ...
20
+ });
21
+ ```
22
+
23
+ In commands:
24
+
25
+ ```typescript
26
+ run: async (r) => {
27
+ await r.tabs([r.exec('npm run dev'), r.exec('stripe listen')], { name: 'Development' });
28
+ };
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - Full-screen tabbed interface
34
+ - Keyboard navigation (arrow keys, numbers)
35
+ - Scrollable output per tab
36
+ - Process lifecycle management
37
+ - Status indicators (running, success, error)
38
+
39
+ ## Documentation
40
+
41
+ See the [full documentation](https://github.com/openpok/pok/blob/main/docs/packages/tabs-ink.md).
@@ -0,0 +1,20 @@
1
+ import type { TabsAdapter, EventBus } from '@pokit/core';
2
+ /**
3
+ * Create a tabs adapter using Ink
4
+ */
5
+ export declare function createTabsAdapter(): TabsAdapter;
6
+ export type EventAdapterOptions = {
7
+ onExit?: (code: number) => void;
8
+ };
9
+ /**
10
+ * Create an event-driven adapter that renders based on EventBus events.
11
+ * This adapter builds a state tree from events and renders it using Ink.
12
+ *
13
+ * @param bus - The EventBus to listen to
14
+ * @param options - Options for the adapter
15
+ * @returns Object with unmount function to stop the adapter
16
+ */
17
+ export declare function createEventAdapter(bus: EventBus, options?: EventAdapterOptions): {
18
+ unmount: () => void;
19
+ };
20
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAwB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAK/E;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CA+G/C;AAMD,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,QAAQ,EACb,OAAO,GAAE,mBAAwB,GAChC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA2EzB"}
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Ink-based Tabs Adapter
4
+ *
5
+ * Implements the TabsAdapter interface using Ink (React for CLI).
6
+ * Also provides an event-driven adapter that renders based on EventBus events.
7
+ */
8
+ import { render } from 'ink';
9
+ import { TabsApp } from './tabs-app.js';
10
+ import { EventDrivenApp } from './event-driven-app.js';
11
+ import { TabsErrorBoundary, restoreTerminal } from './error-boundary.js';
12
+ /**
13
+ * Create a tabs adapter using Ink
14
+ */
15
+ export function createTabsAdapter() {
16
+ return {
17
+ async run(items, options) {
18
+ // Check stdout TTY
19
+ if (!process.stdout.isTTY) {
20
+ throw new Error('Tabbed view requires stdout to be a TTY');
21
+ }
22
+ // Check stdin TTY - required for keyboard input
23
+ if (!process.stdin.isTTY) {
24
+ throw new Error('Tabbed view requires stdin to be a TTY for keyboard input');
25
+ }
26
+ // Empty items - nothing to do
27
+ if (items.length === 0) {
28
+ return;
29
+ }
30
+ // Switch to alternate screen buffer (like vim/less)
31
+ // This preserves the main terminal content and provides a clean canvas
32
+ process.stdout.write('\x1b[?1049h\x1b[H');
33
+ // Ensure stdin is not paused - previous CLI operations (prompts, spinners)
34
+ // may have left it in a paused state
35
+ if (process.stdin.isPaused()) {
36
+ process.stdin.resume();
37
+ }
38
+ // Enable raw mode before Ink starts - Ink's internal ref counting may be
39
+ // out of sync if previous CLI operations (clack prompts/spinners) didn't
40
+ // properly balance their setRawMode calls
41
+ process.stdin.setRawMode(true);
42
+ return new Promise((resolve) => {
43
+ let resolved = false;
44
+ let unmount = null;
45
+ let clear = null;
46
+ // Cleanup function to restore terminal and resolve
47
+ const cleanup = () => {
48
+ if (resolved)
49
+ return;
50
+ resolved = true;
51
+ // Remove signal handlers
52
+ process.removeListener('SIGTERM', handleSignal);
53
+ process.removeListener('SIGQUIT', handleSignal);
54
+ process.removeListener('uncaughtException', handleUncaughtException);
55
+ try {
56
+ clear?.();
57
+ unmount?.();
58
+ }
59
+ catch {
60
+ // Ignore errors during cleanup
61
+ }
62
+ restoreTerminal();
63
+ resolve();
64
+ };
65
+ // Signal handler for graceful shutdown
66
+ const handleSignal = () => {
67
+ cleanup();
68
+ process.exit(0);
69
+ };
70
+ // Handle uncaught exceptions
71
+ const handleUncaughtException = (error) => {
72
+ restoreTerminal();
73
+ console.error('\n[TabsUI] Uncaught exception:', error);
74
+ cleanup();
75
+ process.exit(1);
76
+ };
77
+ // Register signal handlers
78
+ process.on('SIGTERM', handleSignal);
79
+ process.on('SIGQUIT', handleSignal);
80
+ process.on('uncaughtException', handleUncaughtException);
81
+ // Handle fatal errors from error boundary
82
+ const handleFatalError = () => {
83
+ cleanup();
84
+ };
85
+ const result = render(_jsx(TabsErrorBoundary, { onFatalError: handleFatalError, children: _jsx(TabsApp, { items: items, options: options, onExit: () => {
86
+ cleanup();
87
+ } }) }), {
88
+ exitOnCtrlC: false, // We handle quit ourselves
89
+ incrementalRendering: true, // Only update changed lines to reduce flicker
90
+ });
91
+ unmount = result.unmount;
92
+ clear = result.clear;
93
+ // Also resolve when ink exits naturally
94
+ result.waitUntilExit().then(() => {
95
+ if (!resolved) {
96
+ cleanup();
97
+ }
98
+ });
99
+ });
100
+ },
101
+ };
102
+ }
103
+ /**
104
+ * Create an event-driven adapter that renders based on EventBus events.
105
+ * This adapter builds a state tree from events and renders it using Ink.
106
+ *
107
+ * @param bus - The EventBus to listen to
108
+ * @param options - Options for the adapter
109
+ * @returns Object with unmount function to stop the adapter
110
+ */
111
+ export function createEventAdapter(bus, options = {}) {
112
+ if (!process.stdout.isTTY) {
113
+ throw new Error('Event-driven tabs view requires stdout to be a TTY');
114
+ }
115
+ if (!process.stdin.isTTY) {
116
+ throw new Error('Event-driven tabs view requires stdin to be a TTY for keyboard input');
117
+ }
118
+ process.stdout.write('\x1b[?1049h\x1b[H');
119
+ let isCleanedUp = false;
120
+ // Cleanup function to restore terminal
121
+ const cleanup = () => {
122
+ if (isCleanedUp)
123
+ return;
124
+ isCleanedUp = true;
125
+ // Remove signal handlers
126
+ process.removeListener('SIGTERM', handleSignal);
127
+ process.removeListener('SIGQUIT', handleSignal);
128
+ process.removeListener('uncaughtException', handleUncaughtException);
129
+ try {
130
+ clear();
131
+ unmount();
132
+ }
133
+ catch {
134
+ // Ignore errors during cleanup
135
+ }
136
+ restoreTerminal();
137
+ };
138
+ // Signal handler for graceful shutdown
139
+ const handleSignal = () => {
140
+ cleanup();
141
+ process.exit(0);
142
+ };
143
+ // Handle uncaught exceptions
144
+ const handleUncaughtException = (error) => {
145
+ restoreTerminal();
146
+ console.error('\n[TabsUI] Uncaught exception:', error);
147
+ cleanup();
148
+ process.exit(1);
149
+ };
150
+ // Register signal handlers
151
+ process.on('SIGTERM', handleSignal);
152
+ process.on('SIGQUIT', handleSignal);
153
+ process.on('uncaughtException', handleUncaughtException);
154
+ const handleExit = (code) => {
155
+ cleanup();
156
+ options.onExit?.(code);
157
+ };
158
+ // Handle fatal errors from error boundary
159
+ const handleFatalError = () => {
160
+ cleanup();
161
+ };
162
+ const { unmount, clear } = render(_jsx(TabsErrorBoundary, { onFatalError: handleFatalError, children: _jsx(EventDrivenApp, { bus: bus, onExit: handleExit }) }), {
163
+ exitOnCtrlC: false,
164
+ incrementalRendering: true,
165
+ });
166
+ return {
167
+ unmount: cleanup,
168
+ };
169
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Error Boundary for Tabs UI
3
+ *
4
+ * Catches React errors and ensures terminal state is restored before displaying error.
5
+ * This prevents the terminal from being left in a corrupted state (alternate screen,
6
+ * hidden cursor, raw mode) when React rendering crashes.
7
+ */
8
+ import { Component, type ReactNode, type ErrorInfo } from 'react';
9
+ export type ErrorBoundaryProps = {
10
+ children: ReactNode;
11
+ onFatalError?: (error: Error, errorInfo: ErrorInfo) => void;
12
+ };
13
+ type ErrorBoundaryState = {
14
+ hasError: boolean;
15
+ error: Error | null;
16
+ };
17
+ /**
18
+ * Restore terminal to a clean state.
19
+ * Called on errors and signal handlers to ensure terminal isn't left corrupted.
20
+ */
21
+ export declare function restoreTerminal(): void;
22
+ /**
23
+ * React Error Boundary that ensures terminal cleanup on crashes.
24
+ */
25
+ export declare class TabsErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
26
+ constructor(props: ErrorBoundaryProps);
27
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
28
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
29
+ render(): ReactNode;
30
+ }
31
+ export {};
32
+ //# sourceMappingURL=error-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../src/error-boundary.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGlE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;CAC7D,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAgBtC;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;gBAC1E,KAAK,EAAE,kBAAkB;IAKrC,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB;IAIjE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,GAAG,IAAI;IAe3D,MAAM,IAAI,SAAS;CAkBpB"}
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Error Boundary for Tabs UI
4
+ *
5
+ * Catches React errors and ensures terminal state is restored before displaying error.
6
+ * This prevents the terminal from being left in a corrupted state (alternate screen,
7
+ * hidden cursor, raw mode) when React rendering crashes.
8
+ */
9
+ import { Component } from 'react';
10
+ import { Text, Box } from 'ink';
11
+ /**
12
+ * Restore terminal to a clean state.
13
+ * Called on errors and signal handlers to ensure terminal isn't left corrupted.
14
+ */
15
+ export function restoreTerminal() {
16
+ // Exit alternate screen buffer
17
+ process.stdout.write('\x1b[?1049l');
18
+ // Show cursor (in case it was hidden)
19
+ process.stdout.write('\x1b[?25h');
20
+ // Reset all attributes (colors, styles)
21
+ process.stdout.write('\x1b[0m');
22
+ // Try to restore raw mode if stdin is a TTY
23
+ if (process.stdin.isTTY && process.stdin.isRaw) {
24
+ try {
25
+ process.stdin.setRawMode(false);
26
+ }
27
+ catch {
28
+ // Ignore errors - stdin may already be closed
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * React Error Boundary that ensures terminal cleanup on crashes.
34
+ */
35
+ export class TabsErrorBoundary extends Component {
36
+ constructor(props) {
37
+ super(props);
38
+ this.state = { hasError: false, error: null };
39
+ }
40
+ static getDerivedStateFromError(error) {
41
+ return { hasError: true, error };
42
+ }
43
+ componentDidCatch(error, errorInfo) {
44
+ // Restore terminal state immediately
45
+ restoreTerminal();
46
+ // Log error details for debugging
47
+ console.error('\n[TabsUI] Fatal error caught by error boundary:');
48
+ console.error(error);
49
+ if (errorInfo.componentStack) {
50
+ console.error('\nComponent stack:', errorInfo.componentStack);
51
+ }
52
+ // Notify parent if callback provided
53
+ this.props.onFatalError?.(error, errorInfo);
54
+ }
55
+ render() {
56
+ if (this.state.hasError) {
57
+ // Show minimal error message - terminal should already be restored
58
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "red", bold: true, children: "TabsUI encountered a fatal error" }), _jsx(Text, { color: "gray", children: this.state.error?.message ?? 'Unknown error' }), _jsx(Text, { color: "gray", dimColor: true, children: "Press Ctrl+C to exit" })] }));
59
+ }
60
+ return this.props.children;
61
+ }
62
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Event-Driven CLI App
3
+ *
4
+ * A React component that renders CLI state based on EventBus events.
5
+ * Supports different layouts (tabs, parallel, sequence) based on group layout hints.
6
+ */
7
+ import type { EventBus } from '@pokit/core';
8
+ type EventDrivenAppProps = {
9
+ bus: EventBus;
10
+ onExit: (code: number) => void;
11
+ };
12
+ export declare function EventDrivenApp({ bus, onExit }: EventDrivenAppProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=event-driven-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-driven-app.d.ts","sourceRoot":"","sources":["../src/event-driven-app.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAK5C,KAAK,mBAAmB,GAAG;IACzB,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,CAAC;AA8HF,wBAAgB,cAAc,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAyGlE"}
@@ -0,0 +1,128 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Event-Driven CLI App
4
+ *
5
+ * A React component that renders CLI state based on EventBus events.
6
+ * Supports different layouts (tabs, parallel, sequence) based on group layout hints.
7
+ */
8
+ import { useState, useCallback } from 'react';
9
+ import { Box, Text, useInput, useStdout } from 'ink';
10
+ import { findTabsGroup, getTabsGroupActivities } from '@pokit/tabs-core';
11
+ import { useEventBus } from './use-event-bus.js';
12
+ function getStatusIndicator({ status, inverse, }) {
13
+ switch (status) {
14
+ case 'running':
15
+ return { color: inverse ? 'cyanBright' : 'cyan', icon: '●' };
16
+ case 'success':
17
+ return { color: inverse ? 'greenBright' : 'green', icon: '✓' };
18
+ case 'failure':
19
+ return { color: inverse ? 'redBright' : 'red', icon: '✗' };
20
+ }
21
+ }
22
+ function ActivityTabBar({ activities, activeIndex, }) {
23
+ return (_jsx(Box, { gap: 1, flexWrap: "wrap", children: activities.map((activity, i) => {
24
+ const isActive = i === activeIndex;
25
+ const { color, icon } = getStatusIndicator({
26
+ status: activity.status,
27
+ inverse: isActive,
28
+ });
29
+ return (_jsxs(Box, { children: [_jsxs(Text, { inverse: isActive, color: color, children: [' ', icon, ' '] }), _jsxs(Text, { inverse: isActive, children: [" ", activity.label] }), _jsxs(Text, { inverse: isActive, children: [' (', i + 1, ') '] })] }, activity.id));
30
+ }) }));
31
+ }
32
+ function ActivityView({ activity, viewHeight }) {
33
+ const logs = activity.logs.slice(-viewHeight);
34
+ return (_jsxs(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: [activity.message && _jsx(Text, { color: "cyan", children: activity.message }), activity.progress !== undefined && (_jsxs(Text, { color: "yellow", children: ["Progress: ", activity.progress, "%"] })), logs.map((log, i) => {
35
+ let color;
36
+ switch (log.level) {
37
+ case 'error':
38
+ color = 'red';
39
+ break;
40
+ case 'warn':
41
+ color = 'yellow';
42
+ break;
43
+ case 'success':
44
+ color = 'green';
45
+ break;
46
+ case 'info':
47
+ color = 'blue';
48
+ break;
49
+ default:
50
+ color = undefined;
51
+ }
52
+ return (_jsx(Text, { color: color, wrap: "truncate", children: log.message }, i));
53
+ }), Array.from({
54
+ length: Math.max(0, viewHeight -
55
+ logs.length -
56
+ (activity.message ? 1 : 0) -
57
+ (activity.progress !== undefined ? 1 : 0)),
58
+ }).map((_, i) => (_jsx(Text, { children: " " }, `empty-${i}`)))] }));
59
+ }
60
+ function StatusBar({ activityCount, quitConfirmPending, }) {
61
+ if (quitConfirmPending) {
62
+ return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: "yellow", color: "black", children: [' ', "Press q again to quit, any other key to cancel", ' '] }) }));
63
+ }
64
+ return (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["[Tab/1-", activityCount, "] switch | [q]uit"] }) }));
65
+ }
66
+ export function EventDrivenApp({ bus, onExit }) {
67
+ const state = useEventBus(bus);
68
+ const { stdout } = useStdout();
69
+ const [activeIndex, setActiveIndex] = useState(0);
70
+ const [quitConfirmPending, setQuitConfirmPending] = useState(false);
71
+ const terminalHeight = stdout?.rows ?? 24;
72
+ const viewHeight = Math.max(5, terminalHeight - 6);
73
+ const tabsGroup = findTabsGroup(state);
74
+ const activities = tabsGroup
75
+ ? getTabsGroupActivities(state, tabsGroup.id)
76
+ : Array.from(state.activities.values());
77
+ const activeActivity = activities[activeIndex];
78
+ const switchTab = useCallback((newIndex) => {
79
+ if (newIndex >= 0 && newIndex < activities.length) {
80
+ setActiveIndex(newIndex);
81
+ }
82
+ }, [activities.length]);
83
+ const handleQuitRequest = useCallback(() => {
84
+ setQuitConfirmPending((prev) => !prev);
85
+ }, []);
86
+ const handleQuit = useCallback(() => {
87
+ onExit(state.exitCode ?? 0);
88
+ }, [onExit, state.exitCode]);
89
+ useInput((input, key) => {
90
+ if (quitConfirmPending) {
91
+ if (input === 'q') {
92
+ handleQuit();
93
+ }
94
+ else {
95
+ handleQuitRequest();
96
+ }
97
+ return;
98
+ }
99
+ if (input === 'q') {
100
+ handleQuitRequest();
101
+ return;
102
+ }
103
+ if (input === 'c' && key.ctrl) {
104
+ handleQuit();
105
+ return;
106
+ }
107
+ const num = parseInt(input, 10);
108
+ if (num >= 1 && num <= activities.length) {
109
+ switchTab(num - 1);
110
+ return;
111
+ }
112
+ if (key.tab && key.shift) {
113
+ switchTab((activeIndex - 1 + activities.length) % activities.length);
114
+ return;
115
+ }
116
+ if (key.tab) {
117
+ switchTab((activeIndex + 1) % activities.length);
118
+ return;
119
+ }
120
+ }, { isActive: true });
121
+ if (activities.length === 0) {
122
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { children: "Waiting for activities..." }), state.appName && (_jsxs(Text, { dimColor: true, children: [state.appName, state.version ? ` v${state.version}` : ''] }))] }));
123
+ }
124
+ if (!activeActivity) {
125
+ return _jsx(Text, { children: "No active activity" });
126
+ }
127
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(ActivityTabBar, { activities: activities, activeIndex: activeIndex }), _jsx(Box, { borderLeft: false, borderRight: false, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(ActivityView, { activity: activeActivity, viewHeight: viewHeight }) }), _jsx(StatusBar, { activityCount: activities.length, quitConfirmPending: quitConfirmPending })] }));
128
+ }
@@ -0,0 +1,5 @@
1
+ export type HelpOverlayProps = {
2
+ onClose: () => void;
3
+ };
4
+ export declare function HelpOverlay(_props: HelpOverlayProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=help-overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-overlay.d.ts","sourceRoot":"","sources":["../src/help-overlay.tsx"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,2CAgCnD"}
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { HELP_CONTENT } from '@pokit/tabs-core';
4
+ export function HelpOverlay(_props) {
5
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "blue", paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, children: "Keyboard Help" }) }), HELP_CONTENT.map((group, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: i < HELP_CONTENT.length - 1 ? 1 : 0, children: [_jsx(Text, { bold: true, color: "cyan", children: group.title }), group.shortcuts.map(({ key, description }) => (_jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: "yellow", children: key }) }), _jsx(Text, { children: description })] }, key)))] }, group.title))), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "Press ? or Escape to close" }) })] }));
6
+ }
@@ -0,0 +1,6 @@
1
+ export { createTabsAdapter, createEventAdapter } from './adapter.js';
2
+ export type { EventAdapterOptions } from './adapter.js';
3
+ export { useEventBus } from './use-event-bus.js';
4
+ export { createInitialState, reducer, getTabsGroupActivities, findTabsGroup, } from '@pokit/tabs-core';
5
+ export type { ActivityNode, GroupNode, EventDrivenState, TabProcess } from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGjD,OAAO,EACL,kBAAkB,EAClB,OAAO,EACP,sBAAsB,EACtB,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createTabsAdapter, createEventAdapter } from './adapter.js';
2
+ export { useEventBus } from './use-event-bus.js';
3
+ // Re-export state management from @pokit/tabs-core (single source of truth)
4
+ export { createInitialState, reducer, getTabsGroupActivities, findTabsGroup, } from '@pokit/tabs-core';
@@ -0,0 +1,21 @@
1
+ import type { TabProcess } from '@pokit/tabs-core';
2
+ type TabbedViewProps = {
3
+ tabs: TabProcess[];
4
+ activeIndex: number;
5
+ onActiveIndexChange: (index: number) => void;
6
+ onQuit: () => void;
7
+ onQuitRequest: () => void;
8
+ onRestart: (index: number) => void;
9
+ onKill: (index: number) => void;
10
+ quitConfirmPending: boolean;
11
+ focusMode: boolean;
12
+ onEnterFocusMode: () => void;
13
+ onExitFocusMode: () => void;
14
+ onSendInput: (data: string) => void;
15
+ helpVisible: boolean;
16
+ onToggleHelp: () => void;
17
+ onCloseHelp: () => void;
18
+ };
19
+ export declare function TabbedView({ tabs, activeIndex, onActiveIndexChange, onQuit, onQuitRequest, onRestart, onKill, quitConfirmPending, focusMode, onEnterFocusMode, onExitFocusMode, onSendInput, helpVisible, onToggleHelp, onCloseHelp, }: TabbedViewProps): import("react/jsx-runtime").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=tabbed-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabbed-view.d.ts","sourceRoot":"","sources":["../src/tabbed-view.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAWnD,KAAK,eAAe,GAAG;IACrB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAyMF,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,WAAW,EACX,mBAAmB,EACnB,MAAM,EACN,aAAa,EACb,SAAS,EACT,MAAM,EACN,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,GACZ,EAAE,eAAe,2CAqGjB"}
@@ -0,0 +1,137 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useInput, useStdout } from 'ink';
3
+ import { useTabsState, useKeyboardCallbackRefs, processKeyEvent, executeKeyboardAction, } from '@pokit/tabs-core';
4
+ import { HelpOverlay } from './help-overlay.js';
5
+ function getStatusIndicator({ status, inverse, }) {
6
+ switch (status) {
7
+ case 'running':
8
+ return { color: inverse ? 'cyanBright' : 'cyan', icon: '●' };
9
+ case 'done':
10
+ return { color: inverse ? 'greenBright' : 'green', icon: '✓' };
11
+ case 'error':
12
+ return { color: inverse ? 'redBright' : 'red', icon: '✗' };
13
+ case 'stopped':
14
+ return { color: inverse ? 'yellowBright' : 'yellow', icon: '■' };
15
+ }
16
+ }
17
+ function TabBar({ tabs, activeIndex, focusMode, }) {
18
+ return (_jsxs(Box, { gap: 1, flexWrap: "wrap", children: [tabs.map((tab, i) => {
19
+ const isActive = i === activeIndex;
20
+ const { color, icon } = getStatusIndicator({
21
+ status: tab.status,
22
+ inverse: isActive,
23
+ });
24
+ return (_jsxs(Box, { children: [_jsxs(Text, { inverse: isActive, color: color, children: [' ', icon, ' '] }), _jsxs(Text, { inverse: isActive, children: [" ", tab.label] }), _jsxs(Text, { inverse: isActive, children: [' (', i + 1, ') '] })] }, tab.id));
25
+ }), focusMode && (_jsx(Text, { backgroundColor: "yellow", color: "black", children: ' INPUT MODE ' }))] }));
26
+ }
27
+ function OutputView({ lines, scrollOffset, viewHeight, canScrollUp, canScrollDown, }) {
28
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + viewHeight);
29
+ return (_jsxs(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: [visibleLines.map((line, i) => {
30
+ // Show scroll indicator on first two lines
31
+ let prefix = ' ';
32
+ if (i === 0 && canScrollUp)
33
+ prefix = '↑ ';
34
+ else if (i === 1 && canScrollDown)
35
+ prefix = '↓ ';
36
+ return (_jsxs(Text, { wrap: "truncate", dimColor: i < 2 && prefix !== ' ', children: [_jsx(Text, { dimColor: true, children: prefix }), _jsx(Text, { children: line || ' ' })] }, i));
37
+ }), Array.from({
38
+ length: Math.max(0, viewHeight - visibleLines.length),
39
+ }).map((_, i) => {
40
+ const lineIndex = visibleLines.length + i;
41
+ let prefix = ' ';
42
+ if (lineIndex === 0 && canScrollUp)
43
+ prefix = '↑ ';
44
+ else if (lineIndex === 1 && canScrollDown)
45
+ prefix = '↓ ';
46
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: prefix }), _jsx(Text, { children: " " })] }, `empty-${i}`));
47
+ })] }));
48
+ }
49
+ function StatusBar({ tabCount, quitConfirmPending, focusMode, showHelpHint, }) {
50
+ if (quitConfirmPending) {
51
+ return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: "yellow", color: "black", children: [' ', "Press q again to quit, any other key to cancel", ' '] }) }));
52
+ }
53
+ if (focusMode) {
54
+ return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: "cyan", color: "black", children: [' ', "INPUT MODE", ' '] }), _jsx(Text, { dimColor: true, children: " Press Esc to exit input mode" })] }));
55
+ }
56
+ return (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["[\u2191\u2193] scroll | [Tab/1-", tabCount, "] switch | [i]nput | [r]estart | [k]ill | [q]uit", showHelpHint && ' | Press ? for help'] }) }));
57
+ }
58
+ /**
59
+ * Normalize Ink's useInput key event to the shared NormalizedKeyEvent format.
60
+ */
61
+ function normalizeInkKeyEvent(input, key) {
62
+ // Map Ink key properties to normalized key names
63
+ let name;
64
+ if (key.escape)
65
+ name = 'escape';
66
+ else if (key.return)
67
+ name = 'return';
68
+ else if (key.tab)
69
+ name = 'tab';
70
+ else if (key.backspace)
71
+ name = 'backspace';
72
+ else if (key.delete)
73
+ name = 'delete';
74
+ else if (key.upArrow)
75
+ name = 'up';
76
+ else if (key.downArrow)
77
+ name = 'down';
78
+ else if (key.leftArrow)
79
+ name = 'left';
80
+ else if (key.rightArrow)
81
+ name = 'right';
82
+ else if (key.pageUp)
83
+ name = 'pageup';
84
+ else if (key.pageDown)
85
+ name = 'pagedown';
86
+ return {
87
+ char: input || undefined,
88
+ name,
89
+ ctrl: key.ctrl,
90
+ shift: key.shift,
91
+ meta: key.meta,
92
+ };
93
+ }
94
+ export function TabbedView({ tabs, activeIndex, onActiveIndexChange, onQuit, onQuitRequest, onRestart, onKill, quitConfirmPending, focusMode, onEnterFocusMode, onExitFocusMode, onSendInput, helpVisible, onToggleHelp, onCloseHelp, }) {
95
+ const { stdout } = useStdout();
96
+ // Calculate view height based on terminal size
97
+ // Reserve space for: padding (2) + tab bar (1) + border (2) + status bar (1) = 6
98
+ const terminalHeight = stdout?.rows ?? 24;
99
+ const viewHeight = Math.max(5, terminalHeight - 6);
100
+ // Use shared tabs state hook
101
+ const { showHelpHint, activeScrollOffset, canScrollUp, canScrollDown, scrollBy, switchTab, nextTab, prevTab, } = useTabsState({
102
+ tabs,
103
+ activeIndex,
104
+ onActiveIndexChange,
105
+ viewHeight,
106
+ });
107
+ // Use shared callback refs hook
108
+ const callbacks = {
109
+ onQuit,
110
+ onQuitRequest,
111
+ onRestart,
112
+ onKill,
113
+ onEnterFocusMode,
114
+ onExitFocusMode,
115
+ onSendInput,
116
+ onToggleHelp,
117
+ onCloseHelp,
118
+ };
119
+ const callbackRefs = useKeyboardCallbackRefs(callbacks);
120
+ // Keyboard handling via useInput - always active
121
+ useInput((input, key) => {
122
+ const normalizedEvent = normalizeInkKeyEvent(input, key);
123
+ const action = processKeyEvent(normalizedEvent, {
124
+ helpVisible,
125
+ focusMode,
126
+ quitConfirmPending,
127
+ activeIndex,
128
+ tabCount: tabs.length,
129
+ }, viewHeight);
130
+ executeKeyboardAction(action, callbackRefs, scrollBy, switchTab, nextTab, prevTab);
131
+ });
132
+ const activeTab = tabs[activeIndex];
133
+ if (!activeTab) {
134
+ return _jsx(Text, { children: "No tabs available" });
135
+ }
136
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(TabBar, { tabs: tabs, activeIndex: activeIndex, focusMode: focusMode }), _jsx(Box, { borderLeft: false, borderRight: false, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(OutputView, { lines: activeTab.output, scrollOffset: activeScrollOffset, viewHeight: viewHeight, canScrollUp: canScrollUp, canScrollDown: canScrollDown }) }), _jsx(StatusBar, { tabCount: tabs.length, quitConfirmPending: quitConfirmPending, focusMode: focusMode, showHelpHint: showHelpHint }), helpVisible && (_jsx(Box, { position: "absolute", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: _jsx(HelpOverlay, { onClose: onCloseHelp }) }))] }));
137
+ }
@@ -0,0 +1,9 @@
1
+ import type { TabSpec, TabsOptions } from '@pokit/core';
2
+ type TabsAppProps = {
3
+ items: TabSpec[];
4
+ options: TabsOptions;
5
+ onExit: (code: number) => void;
6
+ };
7
+ export declare function TabsApp({ items, options, onExit }: TabsAppProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=tabs-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs-app.d.ts","sourceRoot":"","sources":["../src/tabs-app.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAIxD,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,CAAC;AAOF,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,YAAY,2CA+R/D"}
@@ -0,0 +1,234 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { spawn } from 'node:child_process';
4
+ import { TabbedView } from './tabbed-view.js';
5
+ import { OutputBuffer, MAX_OUTPUT_LINES, MAX_LINE_LENGTH, BUFFER_WARNING_THRESHOLD, } from '@pokit/tabs-core';
6
+ const OUTPUT_BATCH_MS = 16;
7
+ export function TabsApp({ items, options, onExit }) {
8
+ // Ring buffers for storing all output per tab (with O(1) operations)
9
+ const ringBuffersRef = useRef(new Map());
10
+ // Initialize ring buffers for each tab
11
+ const getOrCreateRingBuffer = useCallback((tabId) => {
12
+ let buffer = ringBuffersRef.current.get(tabId);
13
+ if (!buffer) {
14
+ buffer = new OutputBuffer({
15
+ maxLines: MAX_OUTPUT_LINES,
16
+ maxLineLength: MAX_LINE_LENGTH,
17
+ warnAtPercentage: BUFFER_WARNING_THRESHOLD,
18
+ tabId,
19
+ // Optional: could add onPressure callback here for UI feedback
20
+ });
21
+ ringBuffersRef.current.set(tabId, buffer);
22
+ }
23
+ return buffer;
24
+ }, []);
25
+ const [tabs, setTabs] = useState(() => items.map((item, i) => ({
26
+ id: `tab-${i}`,
27
+ label: item.label,
28
+ exec: item.exec,
29
+ output: [],
30
+ status: 'running',
31
+ })));
32
+ const [quitConfirmPending, setQuitConfirmPending] = useState(false);
33
+ const [focusMode, setFocusMode] = useState(false);
34
+ const [activeIndex, setActiveIndex] = useState(0);
35
+ const [helpVisible, setHelpVisible] = useState(false);
36
+ const processesRef = useRef([]);
37
+ const batchBuffersRef = useRef(new Map());
38
+ const flushScheduledRef = useRef(false);
39
+ const flushOutput = useCallback(() => {
40
+ flushScheduledRef.current = false;
41
+ const buffersSnapshot = batchBuffersRef.current;
42
+ batchBuffersRef.current = new Map();
43
+ if (buffersSnapshot.size === 0)
44
+ return;
45
+ const updates = [];
46
+ for (const [index, buffer] of buffersSnapshot.entries()) {
47
+ if (buffer.lines.length > 0) {
48
+ updates.push({ index, lines: buffer.lines });
49
+ }
50
+ }
51
+ if (updates.length === 0)
52
+ return;
53
+ setTabs((prev) => {
54
+ const next = [...prev];
55
+ for (const { index, lines } of updates) {
56
+ const tab = next[index];
57
+ if (!tab)
58
+ continue;
59
+ // Push to ring buffer and get output with dropped indicator
60
+ const ringBuffer = getOrCreateRingBuffer(tab.id);
61
+ ringBuffer.push(...lines);
62
+ next[index] = { ...tab, output: ringBuffer.toArray() };
63
+ }
64
+ return next;
65
+ });
66
+ }, [getOrCreateRingBuffer]);
67
+ const scheduleFlush = useCallback(() => {
68
+ if (flushScheduledRef.current)
69
+ return;
70
+ flushScheduledRef.current = true;
71
+ setTimeout(flushOutput, OUTPUT_BATCH_MS);
72
+ }, [flushOutput]);
73
+ const appendOutput = useCallback((index, data) => {
74
+ const text = data.toString('utf-8');
75
+ const lines = text.split(/\r?\n/).filter((line, idx, arr) => {
76
+ return line || idx < arr.length - 1;
77
+ });
78
+ if (lines.length === 0)
79
+ return;
80
+ let buffer = batchBuffersRef.current.get(index);
81
+ if (!buffer) {
82
+ buffer = { lines: [] };
83
+ batchBuffersRef.current.set(index, buffer);
84
+ }
85
+ buffer.lines.push(...lines);
86
+ scheduleFlush();
87
+ }, [scheduleFlush]);
88
+ const spawnProcess = useCallback((index) => {
89
+ const item = items[index];
90
+ if (!item)
91
+ return null;
92
+ // Use 'pipe' for stdin so we can send input in focus mode
93
+ const proc = spawn('sh', ['-c', item.exec], {
94
+ cwd: options.cwd,
95
+ env: {
96
+ ...options.env,
97
+ FORCE_COLOR: '1',
98
+ },
99
+ stdio: ['pipe', 'pipe', 'pipe'],
100
+ });
101
+ const handleData = (data) => appendOutput(index, data);
102
+ proc.stdout?.on('data', handleData);
103
+ proc.stderr?.on('data', handleData);
104
+ proc.on('close', (code) => {
105
+ flushOutput();
106
+ setTabs((prev) => {
107
+ const next = [...prev];
108
+ const tab = next[index];
109
+ if (!tab)
110
+ return prev;
111
+ next[index] = {
112
+ ...tab,
113
+ status: code === 0 ? 'done' : 'error',
114
+ exitCode: code ?? undefined,
115
+ };
116
+ return next;
117
+ });
118
+ });
119
+ proc.on('error', (err) => {
120
+ setTabs((prev) => {
121
+ const next = [...prev];
122
+ const tab = next[index];
123
+ if (!tab)
124
+ return prev;
125
+ next[index] = {
126
+ ...tab,
127
+ status: 'error',
128
+ output: [...tab.output, `Error: ${err.message}`],
129
+ };
130
+ return next;
131
+ });
132
+ });
133
+ return proc;
134
+ }, [items, options.cwd, options.env, appendOutput, flushOutput]);
135
+ useEffect(() => {
136
+ const procs = items.map((_, i) => spawnProcess(i));
137
+ processesRef.current = procs;
138
+ return () => {
139
+ for (const proc of processesRef.current) {
140
+ if (proc && !proc.killed) {
141
+ proc.kill('SIGTERM');
142
+ }
143
+ }
144
+ };
145
+ }, [items, spawnProcess]);
146
+ const killAll = useCallback(() => {
147
+ for (const proc of processesRef.current) {
148
+ if (proc && !proc.killed) {
149
+ proc.kill('SIGTERM');
150
+ }
151
+ }
152
+ }, []);
153
+ const handleRestart = useCallback((index) => {
154
+ const existingProc = processesRef.current[index];
155
+ if (existingProc && !existingProc.killed) {
156
+ existingProc.kill('SIGTERM');
157
+ }
158
+ // Clear both batch buffer and ring buffer
159
+ batchBuffersRef.current.delete(index);
160
+ const tabId = `tab-${index}`;
161
+ ringBuffersRef.current.get(tabId)?.clear();
162
+ setTabs((prev) => {
163
+ const next = [...prev];
164
+ const tab = next[index];
165
+ if (!tab)
166
+ return prev;
167
+ next[index] = {
168
+ ...tab,
169
+ output: ['Restarting...', ''],
170
+ status: 'running',
171
+ exitCode: undefined,
172
+ };
173
+ return next;
174
+ });
175
+ setTimeout(() => {
176
+ const newProc = spawnProcess(index);
177
+ processesRef.current[index] = newProc;
178
+ }, 100);
179
+ }, [spawnProcess]);
180
+ const handleKill = useCallback((index) => {
181
+ const proc = processesRef.current[index];
182
+ if (proc && !proc.killed) {
183
+ proc.kill('SIGTERM');
184
+ }
185
+ setTabs((prev) => {
186
+ const next = [...prev];
187
+ const tab = next[index];
188
+ if (!tab)
189
+ return prev;
190
+ if (tab.status !== 'running')
191
+ return prev;
192
+ next[index] = {
193
+ ...tab,
194
+ status: 'stopped',
195
+ output: [...tab.output, '', 'Stopped'],
196
+ };
197
+ return next;
198
+ });
199
+ }, []);
200
+ const handleQuitRequest = useCallback(() => {
201
+ setQuitConfirmPending((prev) => !prev);
202
+ }, []);
203
+ const handleQuit = useCallback(() => {
204
+ killAll();
205
+ onExit(0);
206
+ }, [killAll, onExit]);
207
+ const handleEnterFocusMode = useCallback(() => {
208
+ const proc = processesRef.current[activeIndex];
209
+ if (proc && !proc.killed && proc.stdin) {
210
+ setFocusMode(true);
211
+ }
212
+ }, [activeIndex]);
213
+ const handleExitFocusMode = useCallback(() => {
214
+ setFocusMode(false);
215
+ }, []);
216
+ const handleSendInput = useCallback((data) => {
217
+ const proc = processesRef.current[activeIndex];
218
+ if (proc && !proc.killed && proc.stdin) {
219
+ proc.stdin.write(data);
220
+ }
221
+ }, [activeIndex]);
222
+ const handleActiveIndexChange = useCallback((index) => {
223
+ setActiveIndex(index);
224
+ // Exit focus mode when switching tabs
225
+ setFocusMode(false);
226
+ }, []);
227
+ const handleToggleHelp = useCallback(() => {
228
+ setHelpVisible((prev) => !prev);
229
+ }, []);
230
+ const handleCloseHelp = useCallback(() => {
231
+ setHelpVisible(false);
232
+ }, []);
233
+ return (_jsx(TabbedView, { tabs: tabs, activeIndex: activeIndex, onActiveIndexChange: handleActiveIndexChange, onQuit: handleQuit, onQuitRequest: handleQuitRequest, onRestart: handleRestart, onKill: handleKill, quitConfirmPending: quitConfirmPending, focusMode: focusMode, onEnterFocusMode: handleEnterFocusMode, onExitFocusMode: handleExitFocusMode, onSendInput: handleSendInput, helpVisible: helpVisible, onToggleHelp: handleToggleHelp, onCloseHelp: handleCloseHelp }));
234
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Types for tabs-ink
3
+ *
4
+ * Re-exports shared types from @pokit/tabs-core and defines implementation-specific types.
5
+ */
6
+ import type { ChildProcess } from 'node:child_process';
7
+ import type { TabProcess as BaseTabProcess } from '@pokit/tabs-core';
8
+ export type { TabStatus, ActivityNode, GroupNode, EventDrivenState } from '@pokit/tabs-core';
9
+ export { MAX_OUTPUT_LINES } from '@pokit/tabs-core';
10
+ /**
11
+ * Extended TabProcess with ChildProcess reference for process management.
12
+ * This extends the base TabProcess from tabs-core with Ink-specific fields.
13
+ */
14
+ export type TabProcess = BaseTabProcess & {
15
+ /** Reference to the spawned child process (Ink-specific) */
16
+ process?: ChildProcess;
17
+ };
18
+ /**
19
+ * Props for the TabbedView component (UI-specific)
20
+ */
21
+ export type TabbedViewProps = {
22
+ tabs: TabProcess[];
23
+ onQuit: () => void;
24
+ onQuitRequest: () => void;
25
+ quitConfirmPending: boolean;
26
+ };
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMrE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAE7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAMpD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG;IACxC,4DAA4D;IAC5D,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Types for tabs-ink
3
+ *
4
+ * Re-exports shared types from @pokit/tabs-core and defines implementation-specific types.
5
+ */
6
+ export { MAX_OUTPUT_LINES } from '@pokit/tabs-core';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * React Hook for EventBus
3
+ *
4
+ * Provides a hook to subscribe to EventBus events and manage state.
5
+ */
6
+ import type { EventBus } from '@pokit/core';
7
+ import type { EventDrivenState } from '@pokit/tabs-core';
8
+ /**
9
+ * Hook to subscribe to EventBus and manage state
10
+ */
11
+ export declare function useEventBus(bus: EventBus): EventDrivenState;
12
+ //# sourceMappingURL=use-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-event-bus.d.ts","sourceRoot":"","sources":["../src/use-event-bus.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAa3D"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * React Hook for EventBus
3
+ *
4
+ * Provides a hook to subscribe to EventBus events and manage state.
5
+ */
6
+ import { useState, useEffect, useCallback } from 'react';
7
+ import { createInitialState, reducer } from '@pokit/tabs-core';
8
+ /**
9
+ * Hook to subscribe to EventBus and manage state
10
+ */
11
+ export function useEventBus(bus) {
12
+ const [state, setState] = useState(createInitialState);
13
+ const handleEvent = useCallback((event) => {
14
+ setState((prevState) => reducer(prevState, event));
15
+ }, []);
16
+ useEffect(() => {
17
+ const unsubscribe = bus.on(handleEvent);
18
+ return unsubscribe;
19
+ }, [bus, handleEvent]);
20
+ return state;
21
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@pokit/tabs-ink",
3
+ "version": "0.0.1",
4
+ "description": "Ink-based tab renderer for pok CLI applications",
5
+ "keywords": [
6
+ "cli",
7
+ "command-line",
8
+ "typescript",
9
+ "bun",
10
+ "terminal",
11
+ "pok",
12
+ "pokjs",
13
+ "tabs",
14
+ "ink",
15
+ "react",
16
+ "tui"
17
+ ],
18
+ "type": "module",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/notation-dev/openpok.git",
23
+ "directory": "packages/tabs-ink"
24
+ },
25
+ "homepage": "https://github.com/notation-dev/openpok#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/notation-dev/openpok/issues"
28
+ },
29
+ "main": "./src/index.ts",
30
+ "module": "./src/index.ts",
31
+ "types": "./src/index.ts",
32
+ "exports": {
33
+ ".": {
34
+ "bun": "./src/index.ts",
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "ink": "^6.6.0",
49
+ "react": "^19.2.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "@types/react": "^19.2.0",
54
+ "@pokit/core": "0.0.1",
55
+ "@pokit/tabs-core": "0.0.1"
56
+ },
57
+ "peerDependencies": {
58
+ "@pokit/core": "0.0.1",
59
+ "@pokit/tabs-core": "0.0.1"
60
+ },
61
+ "engines": {
62
+ "bun": ">=1.0.0"
63
+ }
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { createTabsAdapter, createEventAdapter } from './adapter.js';
2
+ export type { EventAdapterOptions } from './adapter.js';
3
+
4
+ export { useEventBus } from './use-event-bus.js';
5
+
6
+ // Re-export state management from @pokit/tabs-core (single source of truth)
7
+ export {
8
+ createInitialState,
9
+ reducer,
10
+ getTabsGroupActivities,
11
+ findTabsGroup,
12
+ } from '@pokit/tabs-core';
13
+
14
+ // Re-export types from local types.ts (which re-exports from tabs-core + adds Ink-specific types)
15
+ export type { ActivityNode, GroupNode, EventDrivenState, TabProcess } from './types.js';