@@ -14,7 +14,9 @@ import type { CliRenderer } from "@opentui/core";
1414import type { TuiOptions } from "../index.tsx" ;
1515import type { PipelineAction , StageConfig } from "../model/types.ts" ;
1616import { pipelineReducer , createInitialState } from "../model/reducer.ts" ;
17- import { getCursorStage } from "../model/selectors.ts" ;
17+ import { getCursorStage , getCursorOutput , getDownstreamStages } from "../model/selectors.ts" ;
18+ import { exportAsPipeScript , exportAsChainCommand , copyToClipboard } from "../model/serialization.ts" ;
19+ import { ExportPicker , type ExportFormat } from "./modals/ExportPicker.tsx" ;
1820import { TitleBar } from "./TitleBar.tsx" ;
1921import { StageList } from "./StageList.tsx" ;
2022import { InspectorPanel } from "./InspectorPanel.tsx" ;
@@ -34,7 +36,8 @@ type ModalState =
3436 | { kind : "addStage" }
3537 | { kind : "editStage" }
3638 | { kind : "confirmDelete" ; stageId : string }
37- | { kind : "help" } ;
39+ | { kind : "help" }
40+ | { kind : "exportPicker" } ;
3841
3942export function App ( { options, renderer } : AppProps ) {
4043 const hasInput = Boolean ( options . inputFile || options . sessionId ) ;
@@ -86,6 +89,36 @@ export function App({ options, renderer }: AppProps) {
8689 dispatch ( { type : "TOGGLE_FOCUS" } ) ;
8790 return ;
8891 }
92+ if ( key . name === "c" && key . ctrl ) {
93+ renderer . destroy ( ) ;
94+ return ;
95+ }
96+ if ( key . raw === "x" ) {
97+ const script = exportAsPipeScript ( state ) ;
98+ void copyToClipboard ( script ) . then ( ( ok ) => {
99+ showStatus ( ok ? "Copied pipe script!" : "Export: clipboard failed" ) ;
100+ } ) ;
101+ return ;
102+ }
103+ if ( key . raw === "X" ) {
104+ setModal ( { kind : "exportPicker" } ) ;
105+ return ;
106+ }
107+ if ( key . raw === "v" ) {
108+ const output = getCursorOutput ( state ) ;
109+ if ( output && output . records . length > 0 ) {
110+ const tmpPath = `/tmp/recs-${ Date . now ( ) } .jsonl` ;
111+ const jsonl = output . records . map ( ( r ) => JSON . stringify ( r . toJSON ( ) ) ) . join ( "\n" ) + "\n" ;
112+ void Bun . write ( tmpPath , jsonl ) . then ( ( ) => {
113+ void copyToClipboard ( tmpPath ) . then ( ( ) => {
114+ showStatus ( `Records exported to ${ tmpPath } ` ) ;
115+ } ) ;
116+ } ) ;
117+ } else {
118+ showStatus ( "No records to export" ) ;
119+ }
120+ return ;
121+ }
89122
90123 // Pipeline panel keys
91124 if ( state . focusedPanel === "pipeline" ) {
@@ -119,6 +152,29 @@ export function App({ options, renderer }: AppProps) {
119152 }
120153 return ;
121154 }
155+ if ( key . raw === "r" ) {
156+ if ( state . cursorStageId ) {
157+ dispatch ( { type : "INVALIDATE_STAGE" , stageId : state . cursorStageId } ) ;
158+ const downstream = getDownstreamStages ( state , state . cursorStageId ) ;
159+ for ( const s of downstream ) {
160+ dispatch ( { type : "INVALIDATE_STAGE" , stageId : s . id } ) ;
161+ }
162+ showStatus ( "Re-running from cursor..." ) ;
163+ }
164+ return ;
165+ }
166+ if ( key . raw === "J" ) {
167+ if ( state . cursorStageId ) {
168+ dispatch ( { type : "REORDER_STAGE" , stageId : state . cursorStageId , direction : "down" } ) ;
169+ }
170+ return ;
171+ }
172+ if ( key . raw === "K" ) {
173+ if ( state . cursorStageId ) {
174+ dispatch ( { type : "REORDER_STAGE" , stageId : state . cursorStageId , direction : "up" } ) ;
175+ }
176+ return ;
177+ }
122178 if ( key . name === "return" || key . name === "tab" ) {
123179 dispatch ( { type : "TOGGLE_FOCUS" } ) ;
124180 return ;
@@ -131,6 +187,13 @@ export function App({ options, renderer }: AppProps) {
131187 dispatch ( { type : "TOGGLE_FOCUS" } ) ;
132188 return ;
133189 }
190+ if ( key . raw === "t" ) {
191+ const modes = [ "table" , "prettyprint" , "json" ] as const ;
192+ const currentIdx = modes . indexOf ( state . inspector . viewMode as typeof modes [ number ] ) ;
193+ const nextIdx = ( currentIdx + 1 ) % modes . length ;
194+ dispatch ( { type : "SET_VIEW_MODE" , viewMode : modes [ nextIdx ] ! } ) ;
195+ return ;
196+ }
134197 }
135198 } ) ;
136199
@@ -174,6 +237,31 @@ export function App({ options, renderer }: AppProps) {
174237 setModal ( { kind : "none" } ) ;
175238 } , [ modal , showStatus ] ) ;
176239
240+ const handleExportFormat = useCallback (
241+ ( format : ExportFormat ) => {
242+ setModal ( { kind : "none" } ) ;
243+ if ( format === "pipe-script" ) {
244+ const text = exportAsPipeScript ( state ) ;
245+ void copyToClipboard ( text ) . then ( ( ok ) => {
246+ showStatus ( ok ? "Copied pipe script!" : "Export: clipboard failed" ) ;
247+ } ) ;
248+ } else if ( format === "chain-command" ) {
249+ const text = exportAsChainCommand ( state ) ;
250+ void copyToClipboard ( text ) . then ( ( ok ) => {
251+ showStatus ( ok ? "Copied chain command!" : "Export: clipboard failed" ) ;
252+ } ) ;
253+ } else {
254+ // save-file
255+ const script = exportAsPipeScript ( state ) ;
256+ const tmpPath = `/tmp/recs-pipeline-${ Date . now ( ) } .sh` ;
257+ void Bun . write ( tmpPath , script ) . then ( ( ) => {
258+ showStatus ( `Saved to ${ tmpPath } ` ) ;
259+ } ) ;
260+ }
261+ } ,
262+ [ state , showStatus ] ,
263+ ) ;
264+
177265 if ( ! hasInput ) {
178266 return (
179267 < box flexDirection = "column" width = "100%" height = "100%" >
@@ -230,6 +318,12 @@ export function App({ options, renderer }: AppProps) {
230318 { modal . kind === "help" && (
231319 < HelpPanel onClose = { ( ) => setModal ( { kind : "none" } ) } />
232320 ) }
321+ { modal . kind === "exportPicker" && (
322+ < ExportPicker
323+ onSelect = { handleExportFormat }
324+ onCancel = { ( ) => setModal ( { kind : "none" } ) }
325+ />
326+ ) }
233327 </ box >
234328 ) ;
235329}
0 commit comments