Skip to content

Commit 7034fd8

Browse files
benbernardclaude
andcommitted
feat: major explorer performance overhaul, stream preview, and e2e testing
- Replace structuredClone with fast JSON clone (4x faster record cloning) - Sample-based estimateSize instead of scanning all records (~600x faster) - Stabilize wrappedDispatch with useRef to prevent cascading re-executions - Remove unused useState from useExecution to eliminate full-screen re-renders - Memoize 8 components with React.memo (TitleBar, ForkTabs, StageList, etc.) - Add stream preview to AddStageModal and EditStageModal (Tab to toggle, Enter to zoom) - Split useInput hooks to fix typing garble when TextInput is active - Fix fuzzy search ranking: name-first matching with +200 bonus, minScore threshold - Fix fromps race condition: skip UPDATE_STAGE_ARGS when args unchanged - Fix undo/redo stale cache: clear cache and lastError on UNDO/REDO - Fix execution not retrying after error + args edit (stageFingerprint dep) - Fix disabled first stage showing 0 records - Fix welcome screen 'n' key not working - Rename tui → explorer throughout codebase - Add tmux-based e2e test harness and 10 smoke tests (RUN_E2E=1 to enable) - Add comprehensive unit tests: executor, reducer, fuzzy-match, unicode, etc. - Add executor benchmark script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bdfdc71 commit 7034fd8

137 files changed

Lines changed: 15713 additions & 1657 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/recs-tui.ts

Lines changed: 0 additions & 65 deletions
This file was deleted.

bin/recs.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,79 @@ if (command === "story") {
9999
process.exit(0);
100100
}
101101

102-
// Handle TUI subcommand
103-
if (command === "tui") {
104-
const tuiArgs = args.slice(1);
105-
// Re-exec via recs-tui.ts with remaining args
106-
const { join } = await import("node:path");
107-
const tuiEntry = join(import.meta.dir, "recs-tui.ts");
108-
const proc = Bun.spawn(["bun", "run", tuiEntry, ...tuiArgs], {
109-
stdin: "inherit",
110-
stdout: "inherit",
111-
stderr: "inherit",
112-
});
113-
const code = await proc.exited;
114-
process.exit(code);
102+
// Handle Explorer subcommand
103+
if (command === "explorer") {
104+
const explorerArgs = args.slice(1);
105+
// Dynamically import the explorer entry point so it works in compiled binaries
106+
const explorerModule = await import("../src/explorer/index.tsx");
107+
const launchExplorer = explorerModule.launchExplorer;
108+
type ExplorerOptions = import("../src/explorer/index.tsx").ExplorerOptions;
109+
const { SessionManager } = await import("../src/explorer/session/session-manager.ts");
110+
111+
const sessionManager = new SessionManager();
112+
113+
// Handle --list
114+
if (explorerArgs.includes("--list")) {
115+
const sessions = await sessionManager.list();
116+
if (sessions.length === 0) {
117+
console.log("No saved sessions.");
118+
} else {
119+
console.log("Saved sessions:\n");
120+
for (const s of sessions) {
121+
const label = s.name || s.sessionId;
122+
console.log(` ${label}`);
123+
console.log(` ${s.stageCount} stages, last used ${formatAge(Date.now() - s.lastAccessedAt)}`);
124+
console.log();
125+
}
126+
}
127+
process.exit(0);
128+
}
129+
130+
// Handle --clean
131+
if (explorerArgs.includes("--clean")) {
132+
const removed = await sessionManager.clean();
133+
console.log(removed === 0
134+
? "No sessions to clean up."
135+
: `Removed ${removed} session${removed === 1 ? "" : "s"} older than 7 days.`);
136+
process.exit(0);
137+
}
138+
139+
// Parse explorer options
140+
const explorerOptions: ExplorerOptions = {};
141+
for (let i = 0; i < explorerArgs.length; i++) {
142+
const arg = explorerArgs[i]!;
143+
if (arg === "--session" || arg === "-s") {
144+
explorerOptions.sessionId = explorerArgs[++i];
145+
} else if (arg === "--pipeline" || arg === "-p") {
146+
explorerOptions.pipeline = explorerArgs[++i];
147+
} else if (arg === "--help" || arg === "-h") {
148+
console.log(`Usage: recs explorer [options] [inputfile]
149+
150+
Options:
151+
--session, -s <id> Resume a saved session
152+
--pipeline, -p <cmd> Start with an initial pipeline command
153+
--list List saved sessions
154+
--clean Remove sessions older than 7 days
155+
156+
Supported file types: .csv, .tsv, .xml, .jsonl, .json, .ndjson`);
157+
process.exit(0);
158+
} else if (!arg.startsWith("-")) {
159+
explorerOptions.inputFile = arg;
160+
}
161+
}
162+
163+
try {
164+
await launchExplorer(explorerOptions);
165+
} catch (err) {
166+
process.stderr.write(
167+
`\nExplorer error: ${err instanceof Error ? err.message : String(err)}\n`,
168+
);
169+
if (err instanceof Error && err.stack) {
170+
process.stderr.write(err.stack + "\n");
171+
}
172+
process.exit(1);
173+
}
174+
process.exit(0);
115175
}
116176

117177
// Handle alias management subcommand
@@ -194,3 +254,10 @@ if (!noUpdateCheck && shouldCheck(getConfigDir())) {
194254
}
195255

196256
process.exit(exitCode);
257+
258+
function formatAge(ms: number): string {
259+
if (ms < 60_000) return "just now";
260+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
261+
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`;
262+
return `${Math.floor(ms / 86_400_000)}d ago`;
263+
}

0 commit comments

Comments
 (0)