procsi 0.8.1 → 0.9.0
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/README.md +35 -9
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +63 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/tui/App.d.ts.map +1 -1
- package/dist/cli/tui/App.js +126 -113
- package/dist/cli/tui/App.js.map +1 -1
- package/dist/cli/tui/components/Panel.d.ts +4 -0
- package/dist/cli/tui/components/Panel.d.ts.map +1 -1
- package/dist/cli/tui/components/Panel.js +33 -12
- package/dist/cli/tui/components/Panel.js.map +1 -1
- package/dist/cli/tui/components/RequestList.d.ts +4 -0
- package/dist/cli/tui/components/RequestList.d.ts.map +1 -1
- package/dist/cli/tui/components/RequestList.js +18 -2
- package/dist/cli/tui/components/RequestList.js.map +1 -1
- package/dist/cli/tui/components/StatusBar.d.ts.map +1 -1
- package/dist/cli/tui/components/StatusBar.js +1 -1
- package/dist/cli/tui/components/StatusBar.js.map +1 -1
- package/dist/cli/tui/hooks/useRequestListState.d.ts +23 -0
- package/dist/cli/tui/hooks/useRequestListState.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useRequestListState.js +88 -0
- package/dist/cli/tui/hooks/useRequestListState.js.map +1 -0
- package/dist/cli/tui/hooks/useRequests.d.ts.map +1 -1
- package/dist/cli/tui/hooks/useRequests.js +201 -55
- package/dist/cli/tui/hooks/useRequests.js.map +1 -1
- package/dist/cli/tui/state/request-list-state.d.ts +17 -0
- package/dist/cli/tui/state/request-list-state.d.ts.map +1 -0
- package/dist/cli/tui/state/request-list-state.js +45 -0
- package/dist/cli/tui/state/request-list-state.js.map +1 -0
- package/dist/daemon/control.d.ts.map +1 -1
- package/dist/daemon/control.js +15 -0
- package/dist/daemon/control.js.map +1 -1
- package/dist/daemon/storage.d.ts +16 -1
- package/dist/daemon/storage.d.ts.map +1 -1
- package/dist/daemon/storage.js +136 -16
- package/dist/daemon/storage.js.map +1 -1
- package/dist/shared/control-client.d.ts +9 -1
- package/dist/shared/control-client.d.ts.map +1 -1
- package/dist/shared/control-client.js +6 -0
- package/dist/shared/control-client.js.map +1 -1
- package/dist/shared/types.d.ts +17 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Procsi is a terminal-based, project-scoped HTTP proxy with a powerful CLI & MCP
|
|
|
12
12
|
|
|
13
13
|
- **Project-scoped** — each project gets its own `.procsi/` directory with a separate daemon, database, CA cert and interceptors
|
|
14
14
|
- **MCP server** — AI agents get full access to your captured traffic and can search, filter, inspect, mock — all via tool calls.
|
|
15
|
-
- **Interceptors** — mock, modify or observe traffic by writing typescript. Agent can do this over MCP meaning you can express complex scenarios in natural language!
|
|
15
|
+
- **Interceptors** — mock, modify or observe traffic by writing typescript. Agent can do this over MCP meaning you can express complex scenarios in natural language!
|
|
16
16
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
@@ -27,6 +27,9 @@ curl https://api.example.com/users
|
|
|
27
27
|
|
|
28
28
|
# Open UI
|
|
29
29
|
procsi tui
|
|
30
|
+
|
|
31
|
+
# Add MCP server to your AI tool
|
|
32
|
+
claude mcp add procsi -- procsi mcp
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
## Browser Interception
|
|
@@ -81,19 +84,38 @@ procsi --config /tmp/my-procsi-data on
|
|
|
81
84
|
|
|
82
85
|
See [CLI Reference](docs/cli-reference.md) for the full resolution order (`--config` > `--dir` > auto-detect).
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- AI analysis
|
|
87
|
-
- Chaos monkey
|
|
88
|
-
- Mock out APIs that do not yet exist
|
|
89
|
-
|
|
90
|
-
## MCP Integration
|
|
87
|
+
#### MCP Integration
|
|
91
88
|
|
|
92
89
|
procsi has a built-in [MCP](https://modelcontextprotocol.io/) server that gives AI agents full access to your captured traffic and interceptor system.
|
|
93
90
|
|
|
94
91
|
### Setup
|
|
95
92
|
|
|
96
|
-
|
|
93
|
+
**Claude Code:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add procsi -- procsi mcp
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Codex:**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
codex mcp add procsi -- procsi mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Cursor** — add to `.cursor/mcp.json`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"procsi": {
|
|
111
|
+
"command": "procsi",
|
|
112
|
+
"args": ["mcp"]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Other MCP clients** (Windsurf, etc.) — add to your client's MCP config:
|
|
97
119
|
|
|
98
120
|
```json
|
|
99
121
|
{
|
|
@@ -310,6 +332,10 @@ Your HTTP client needs to respect proxy environment variables.
|
|
|
310
332
|
|
|
311
333
|
There are workarounds implemented for node - e.g. fetch override. Other libraries in different environments may need a similar treatment.
|
|
312
334
|
|
|
335
|
+
## Acknowledgements
|
|
336
|
+
|
|
337
|
+
procsi is built on top of [MockTTP](https://github.com/httptoolkit/mockttp) by [Tim Perry](https://github.com/pimterry), the same MITM proxy engine that powers [HTTP Toolkit](https://httptoolkit.com/).
|
|
338
|
+
|
|
313
339
|
## Licence
|
|
314
340
|
|
|
315
341
|
AGPL-3.0-or-later
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqEpC,eAAO,MAAM,UAAU,SA4BnB,CAAC"}
|
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { createProcsiMcpServer } from "../../mcp/server.js";
|
|
3
3
|
import { getGlobalOptions, requireProjectRoot } from "./helpers.js";
|
|
4
|
+
const SEPARATOR_WIDTH = 48;
|
|
5
|
+
function printSetupInstructions() {
|
|
6
|
+
console.log("procsi MCP server");
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log("Add procsi to your AI tool to give it access to captured HTTP traffic.");
|
|
9
|
+
console.log("");
|
|
10
|
+
const clients = [
|
|
11
|
+
{
|
|
12
|
+
name: "Claude Code",
|
|
13
|
+
lines: [" claude mcp add procsi -- procsi mcp"],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "Cursor",
|
|
17
|
+
lines: [
|
|
18
|
+
" Add to .cursor/mcp.json:",
|
|
19
|
+
"",
|
|
20
|
+
" {",
|
|
21
|
+
' "mcpServers": {',
|
|
22
|
+
' "procsi": {',
|
|
23
|
+
' "command": "procsi",',
|
|
24
|
+
' "args": ["mcp"]',
|
|
25
|
+
" }",
|
|
26
|
+
" }",
|
|
27
|
+
" }",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Codex",
|
|
32
|
+
lines: [" codex mcp add procsi -- procsi mcp"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Other (Windsurf, etc.)",
|
|
36
|
+
lines: [
|
|
37
|
+
" Add to your MCP client config:",
|
|
38
|
+
"",
|
|
39
|
+
" {",
|
|
40
|
+
' "mcpServers": {',
|
|
41
|
+
' "procsi": {',
|
|
42
|
+
' "command": "procsi",',
|
|
43
|
+
' "args": ["mcp"]',
|
|
44
|
+
" }",
|
|
45
|
+
" }",
|
|
46
|
+
" }",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
for (const client of clients) {
|
|
51
|
+
const label = ` ${client.name} `;
|
|
52
|
+
const padding = "\u2500".repeat(Math.max(0, SEPARATOR_WIDTH - label.length - 2));
|
|
53
|
+
console.log(`\u2500\u2500${label}${padding}`);
|
|
54
|
+
console.log("");
|
|
55
|
+
for (const line of client.lines) {
|
|
56
|
+
console.log(line);
|
|
57
|
+
}
|
|
58
|
+
console.log("");
|
|
59
|
+
}
|
|
60
|
+
console.log('Note: The proxy must be running (eval "$(procsi on)") for the MCP server to connect.');
|
|
61
|
+
}
|
|
4
62
|
export const mcpCommand = new Command("mcp")
|
|
5
63
|
.description("Start the procsi MCP server (stdio transport for AI tool integration)")
|
|
6
64
|
.action(async (_, command) => {
|
|
65
|
+
// If stdout is a TTY, user ran directly — show setup instructions instead
|
|
66
|
+
if (process.stdout.isTTY) {
|
|
67
|
+
printSetupInstructions();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
7
70
|
const globalOpts = getGlobalOptions(command);
|
|
8
71
|
const projectRoot = requireProjectRoot(globalOpts.dir);
|
|
9
72
|
const mcp = createProcsiMcpServer({ projectRoot });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../../src/cli/commands/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC;KACzC,WAAW,CAAC,uEAAuE,CAAC;KACpF,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,OAAgB,EAAE,EAAE;IACpC,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,kBAAkB,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAEvD,MAAM,GAAG,GAAG,qBAAqB,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC;IAEnD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAElB,+DAA+D;IAC/D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,WAAW,KAAK,CAAC,CAAC;AAChF,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../../src/cli/commands/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/App.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/App.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAqCjF,UAAU,QAAQ;IAChB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAIpC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,cAAc,OAAO,CAAC;AACnC,eAAO,MAAM,cAAc,OAAO,CAAC;AACnC,eAAO,MAAM,UAAU,OAAO,CAAC;AAs8B/B,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC,YAAY,CAMvD"}
|
package/dist/cli/tui/App.js
CHANGED
|
@@ -2,11 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Root TUI component for browsing captured HTTP traffic.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
6
6
|
import { Box, Text, useInput, useApp, useStdin } from "ink";
|
|
7
7
|
import { MouseProvider, useOnClick, useOnWheel, useOnMouseEnter, useOnMouseLeave } from "@ink-tools/ink-mouse";
|
|
8
8
|
import { useStdoutDimensions } from "./hooks/useStdoutDimensions.js";
|
|
9
9
|
import { useRequests } from "./hooks/useRequests.js";
|
|
10
|
+
import { useRequestListState } from "./hooks/useRequestListState.js";
|
|
10
11
|
import { useSpinner } from "./hooks/useSpinner.js";
|
|
11
12
|
import { useBodyExport, generateFilename } from "./hooks/useBodyExport.js";
|
|
12
13
|
import { formatSize } from "./utils/formatters.js";
|
|
@@ -66,15 +67,10 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
66
67
|
const startTime = useMemo(() => Date.now(), []);
|
|
67
68
|
const { saveBody } = useBodyExport();
|
|
68
69
|
const spinnerFrame = useSpinner(isLoading && requests.length === 0);
|
|
69
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
70
70
|
const [activePanel, setActivePanel] = useState("list");
|
|
71
71
|
const [statusMessage, setStatusMessage] = useState();
|
|
72
72
|
const [showFullUrl, setShowFullUrl] = useState(false);
|
|
73
73
|
const [hoveredPanel, setHoveredPanel] = useState(null);
|
|
74
|
-
const [listScrollOffset, setListScrollOffset] = useState(0);
|
|
75
|
-
// Follow mode — when true, cursor tracks the newest request (index 0)
|
|
76
|
-
const [following, setFollowing] = useState(true);
|
|
77
|
-
const selectedRequestIdRef = useRef(null);
|
|
78
74
|
// Accordion state — independent expand/collapse per section
|
|
79
75
|
const [focusedSection, setFocusedSection] = useState(SECTION_REQUEST);
|
|
80
76
|
const [expandedSections, setExpandedSections] = useState(new Set(ALL_SECTIONS_EXPANDED));
|
|
@@ -118,75 +114,80 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
118
114
|
// Ref to track whether a request is selected (for stable callbacks)
|
|
119
115
|
const hasSelectedRequestRef = useRef(selectedFullRequest !== null);
|
|
120
116
|
hasSelectedRequestRef.current = selectedFullRequest !== null;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
117
|
+
const filterBarHeight = showFilter ? 2 : 0;
|
|
118
|
+
const infoBarHeight = 1;
|
|
119
|
+
const contentHeight = rows - 2 - infoBarHeight - filterBarHeight;
|
|
120
|
+
const visibleListHeight = Math.max(1, contentHeight - 2);
|
|
121
|
+
const { selectedRequestId, setSelectedRequestId, setTopVisibleRequestId, pendingNewCount, setPendingNewCount, following, setFollowing, selectedIndex, effectiveListScrollOffset, selectedSummary, resetToFollowMode, } = useRequestListState({ requests, visibleListHeight });
|
|
122
|
+
// Refs for wheel/input handlers to avoid stale closures.
|
|
123
|
+
// (Ink input handlers may keep callback identity between renders.)
|
|
124
|
+
const contentHeightRef = useRef(contentHeight);
|
|
125
|
+
contentHeightRef.current = contentHeight;
|
|
125
126
|
const requestsLengthRef = useRef(requests.length);
|
|
126
127
|
requestsLengthRef.current = requests.length;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const targetId = selectedRequestIdRef.current;
|
|
139
|
-
if (!targetId || requests.length === 0)
|
|
140
|
-
return;
|
|
141
|
-
const newIndex = requests.findIndex((r) => r.id === targetId);
|
|
142
|
-
if (newIndex !== -1 && newIndex !== selectedIndex) {
|
|
143
|
-
const indexDelta = newIndex - selectedIndex;
|
|
144
|
-
setSelectedIndex(newIndex);
|
|
145
|
-
setListScrollOffset((prev) => Math.max(0, prev + indexDelta));
|
|
146
|
-
}
|
|
147
|
-
}, [requests]);
|
|
128
|
+
const followingRef = useRef(following);
|
|
129
|
+
followingRef.current = following;
|
|
130
|
+
const requestsRef = useRef(requests);
|
|
131
|
+
requestsRef.current = requests;
|
|
132
|
+
const effectiveListScrollOffsetRef = useRef(0);
|
|
133
|
+
effectiveListScrollOffsetRef.current = effectiveListScrollOffset;
|
|
134
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
135
|
+
selectedIndexRef.current = selectedIndex;
|
|
148
136
|
// Stores the filter state at the moment the filter bar opens, so Escape can revert
|
|
149
137
|
const preOpenFilterRef = useRef({});
|
|
150
138
|
const preOpenBodySearchRef = useRef(undefined);
|
|
151
139
|
// Handle filter change from the filter bar
|
|
152
140
|
const handleFilterChange = useCallback((newFilter) => {
|
|
153
141
|
setFilter(newFilter);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
setFollowing(true);
|
|
157
|
-
}, []);
|
|
142
|
+
resetToFollowMode();
|
|
143
|
+
}, [resetToFollowMode]);
|
|
158
144
|
const handleBodySearchChange = useCallback((nextBodySearch) => {
|
|
159
145
|
setBodySearch(nextBodySearch);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
setFollowing(true);
|
|
163
|
-
}, []);
|
|
146
|
+
resetToFollowMode();
|
|
147
|
+
}, [resetToFollowMode]);
|
|
164
148
|
// Handle filter cancel — revert to pre-open state
|
|
165
149
|
const handleFilterCancel = useCallback(() => {
|
|
166
150
|
setFilter(preOpenFilterRef.current);
|
|
167
151
|
setBodySearch(preOpenBodySearchRef.current);
|
|
168
|
-
|
|
169
|
-
selectedRequestIdRef.current = null;
|
|
170
|
-
setFollowing(true);
|
|
152
|
+
resetToFollowMode();
|
|
171
153
|
setShowFilter(false);
|
|
172
|
-
}, []);
|
|
154
|
+
}, [resetToFollowMode]);
|
|
173
155
|
// Handle item click from the request list
|
|
174
156
|
const handleItemClick = useCallback((index) => {
|
|
175
|
-
|
|
176
|
-
if (
|
|
157
|
+
const selected = requests[index];
|
|
158
|
+
if (!selected) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (following) {
|
|
177
162
|
setFollowing(false);
|
|
178
|
-
|
|
163
|
+
setTopVisibleRequestId(requests[effectiveListScrollOffsetRef.current]?.id ?? requests[0]?.id ?? null);
|
|
164
|
+
}
|
|
165
|
+
setSelectedRequestId(selected.id);
|
|
179
166
|
setActivePanel("list");
|
|
180
167
|
}, [requests, following]);
|
|
181
|
-
|
|
168
|
+
const selectedDetailRequestIdRef = useRef(null);
|
|
169
|
+
// Fetch full request data when selection changes.
|
|
170
|
+
// Guard against stale async responses when selection changes rapidly.
|
|
182
171
|
useEffect(() => {
|
|
183
|
-
if (selectedSummary) {
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
172
|
+
if (!selectedSummary) {
|
|
173
|
+
selectedDetailRequestIdRef.current = null;
|
|
187
174
|
setSelectedFullRequest(null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// In browse mode, wait for an explicit ID anchor before fetching detail.
|
|
178
|
+
// This avoids transient fetches for index 0 during follow->browse transitions.
|
|
179
|
+
if (!following && !selectedRequestId) {
|
|
180
|
+
return;
|
|
188
181
|
}
|
|
189
|
-
|
|
182
|
+
const requestId = selectedSummary.id;
|
|
183
|
+
selectedDetailRequestIdRef.current = requestId;
|
|
184
|
+
void getFullRequest(requestId).then((fullRequest) => {
|
|
185
|
+
if (selectedDetailRequestIdRef.current !== requestId) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setSelectedFullRequest(fullRequest);
|
|
189
|
+
});
|
|
190
|
+
}, [selectedSummary?.id, getFullRequest, following, selectedRequestId]);
|
|
190
191
|
// Reset all sections to expanded when a new request is selected
|
|
191
192
|
useEffect(() => {
|
|
192
193
|
if (selectedSummary) {
|
|
@@ -198,11 +199,19 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
198
199
|
// Use refs to avoid stale closures if useOnWheel caches the callback
|
|
199
200
|
const visibleHeight = Math.max(1, contentHeightRef.current - 2);
|
|
200
201
|
const maxOffset = Math.max(0, requestsLengthRef.current - visibleHeight);
|
|
202
|
+
const currentOffset = effectiveListScrollOffsetRef.current;
|
|
203
|
+
const currentRequests = requestsRef.current;
|
|
204
|
+
if (followingRef.current) {
|
|
205
|
+
setFollowing(false);
|
|
206
|
+
setTopVisibleRequestId(currentRequests[currentOffset]?.id ?? currentRequests[0]?.id ?? null);
|
|
207
|
+
}
|
|
201
208
|
if (event.button === "wheel-up") {
|
|
202
|
-
|
|
209
|
+
const nextOffset = Math.max(currentOffset - 1, 0);
|
|
210
|
+
setTopVisibleRequestId(currentRequests[nextOffset]?.id ?? null);
|
|
203
211
|
}
|
|
204
212
|
else if (event.button === "wheel-down") {
|
|
205
|
-
|
|
213
|
+
const nextOffset = Math.min(currentOffset + 1, maxOffset);
|
|
214
|
+
setTopVisibleRequestId(currentRequests[nextOffset]?.id ?? null);
|
|
206
215
|
}
|
|
207
216
|
});
|
|
208
217
|
// Handle click on panels to activate them
|
|
@@ -329,8 +338,7 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
329
338
|
if (pendingClear) {
|
|
330
339
|
setPendingClear(false);
|
|
331
340
|
if (input === "y") {
|
|
332
|
-
|
|
333
|
-
setFollowing(true);
|
|
341
|
+
resetToFollowMode();
|
|
334
342
|
void clearRequests().then((success) => {
|
|
335
343
|
showStatus(success ? "Requests cleared (bookmarks preserved)" : "Failed to clear requests");
|
|
336
344
|
});
|
|
@@ -343,11 +351,14 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
343
351
|
// Navigation - behaviour depends on active panel
|
|
344
352
|
if (input === "j" || key.downArrow) {
|
|
345
353
|
if (activePanel === "list") {
|
|
346
|
-
|
|
354
|
+
const currentRequests = requestsRef.current;
|
|
355
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
356
|
+
const newIdx = Math.min(currentIndex + 1, currentRequests.length - 1);
|
|
357
|
+
if (followingRef.current) {
|
|
347
358
|
setFollowing(false);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
359
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
360
|
+
}
|
|
361
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
351
362
|
}
|
|
352
363
|
else {
|
|
353
364
|
// Navigate sections in accordion
|
|
@@ -356,11 +367,14 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
356
367
|
}
|
|
357
368
|
else if (input === "k" || key.upArrow) {
|
|
358
369
|
if (activePanel === "list") {
|
|
359
|
-
|
|
370
|
+
const currentRequests = requestsRef.current;
|
|
371
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
372
|
+
const newIdx = Math.max(currentIndex - 1, 0);
|
|
373
|
+
if (followingRef.current) {
|
|
360
374
|
setFollowing(false);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
375
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
376
|
+
}
|
|
377
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
364
378
|
}
|
|
365
379
|
else {
|
|
366
380
|
// Navigate sections in accordion
|
|
@@ -370,9 +384,7 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
370
384
|
else if (input === "g" && !key.shift) {
|
|
371
385
|
// Jump to first item/section — re-enters follow mode in list
|
|
372
386
|
if (activePanel === "list") {
|
|
373
|
-
|
|
374
|
-
setSelectedIndex(0);
|
|
375
|
-
setFollowing(true);
|
|
387
|
+
resetToFollowMode();
|
|
376
388
|
}
|
|
377
389
|
else {
|
|
378
390
|
setFocusedSection(SECTION_REQUEST);
|
|
@@ -381,11 +393,13 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
381
393
|
else if (input === "G") {
|
|
382
394
|
// Jump to last item/section
|
|
383
395
|
if (activePanel === "list") {
|
|
384
|
-
const
|
|
385
|
-
|
|
396
|
+
const currentRequests = requestsRef.current;
|
|
397
|
+
const lastIdx = Math.max(0, currentRequests.length - 1);
|
|
398
|
+
if (followingRef.current) {
|
|
386
399
|
setFollowing(false);
|
|
387
|
-
|
|
388
|
-
|
|
400
|
+
}
|
|
401
|
+
setTopVisibleRequestId(currentRequests[Math.max(0, lastIdx - visibleListHeight + 1)]?.id ?? currentRequests[0]?.id ?? null);
|
|
402
|
+
setSelectedRequestId(currentRequests[lastIdx]?.id ?? null);
|
|
389
403
|
}
|
|
390
404
|
else {
|
|
391
405
|
setFocusedSection(SECTION_RESPONSE_BODY);
|
|
@@ -394,23 +408,29 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
394
408
|
else if (input === "u" && key.ctrl) {
|
|
395
409
|
// Half-page up (list only)
|
|
396
410
|
if (activePanel === "list") {
|
|
411
|
+
const currentRequests = requestsRef.current;
|
|
412
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
397
413
|
const halfPage = Math.floor(contentHeightRef.current / 2);
|
|
398
|
-
const newIdx = Math.max(
|
|
399
|
-
if (
|
|
414
|
+
const newIdx = Math.max(currentIndex - halfPage, 0);
|
|
415
|
+
if (followingRef.current) {
|
|
400
416
|
setFollowing(false);
|
|
401
|
-
|
|
402
|
-
|
|
417
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
418
|
+
}
|
|
419
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
403
420
|
}
|
|
404
421
|
}
|
|
405
422
|
else if (input === "d" && key.ctrl) {
|
|
406
423
|
// Half-page down (list only)
|
|
407
424
|
if (activePanel === "list") {
|
|
425
|
+
const currentRequests = requestsRef.current;
|
|
426
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
408
427
|
const halfPage = Math.floor(contentHeightRef.current / 2);
|
|
409
|
-
const newIdx = Math.min(
|
|
410
|
-
if (
|
|
428
|
+
const newIdx = Math.min(currentIndex + halfPage, currentRequests.length - 1);
|
|
429
|
+
if (followingRef.current) {
|
|
411
430
|
setFollowing(false);
|
|
412
|
-
|
|
413
|
-
|
|
431
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
432
|
+
}
|
|
433
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
414
434
|
}
|
|
415
435
|
}
|
|
416
436
|
else if (key.tab) {
|
|
@@ -569,33 +589,47 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
569
589
|
else if (input === "F" && !key.ctrl) {
|
|
570
590
|
// Toggle follow mode
|
|
571
591
|
setFollowing((prev) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
592
|
+
const next = !prev;
|
|
593
|
+
if (next) {
|
|
594
|
+
setSelectedRequestId(null);
|
|
595
|
+
setTopVisibleRequestId(null);
|
|
596
|
+
setPendingNewCount(0);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const currentRequests = requestsRef.current;
|
|
600
|
+
const anchorOffset = effectiveListScrollOffsetRef.current;
|
|
601
|
+
setSelectedRequestId(currentRequests[Math.max(0, selectedIndexRef.current)]?.id ?? currentRequests[0]?.id ?? null);
|
|
602
|
+
setTopVisibleRequestId(currentRequests[anchorOffset]?.id ?? currentRequests[0]?.id ?? null);
|
|
575
603
|
}
|
|
576
|
-
return
|
|
604
|
+
return next;
|
|
577
605
|
});
|
|
578
606
|
}
|
|
579
607
|
else if (input === "f" && key.ctrl) {
|
|
580
608
|
// Full-page down (list only)
|
|
581
609
|
if (activePanel === "list") {
|
|
610
|
+
const currentRequests = requestsRef.current;
|
|
582
611
|
const fullPage = contentHeightRef.current;
|
|
583
|
-
const
|
|
584
|
-
|
|
612
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
613
|
+
const newIdx = Math.min(currentIndex + fullPage, requestsLengthRef.current - 1);
|
|
614
|
+
if (followingRef.current) {
|
|
585
615
|
setFollowing(false);
|
|
586
|
-
|
|
587
|
-
|
|
616
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
617
|
+
}
|
|
618
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
588
619
|
}
|
|
589
620
|
}
|
|
590
621
|
else if (input === "b" && key.ctrl) {
|
|
591
622
|
// Full-page up (list only)
|
|
592
623
|
if (activePanel === "list") {
|
|
624
|
+
const currentRequests = requestsRef.current;
|
|
593
625
|
const fullPage = contentHeightRef.current;
|
|
594
|
-
const
|
|
595
|
-
|
|
626
|
+
const currentIndex = Math.max(0, selectedIndexRef.current);
|
|
627
|
+
const newIdx = Math.max(currentIndex - fullPage, 0);
|
|
628
|
+
if (followingRef.current) {
|
|
596
629
|
setFollowing(false);
|
|
597
|
-
|
|
598
|
-
|
|
630
|
+
setTopVisibleRequestId(currentRequests[effectiveListScrollOffsetRef.current]?.id ?? currentRequests[0]?.id ?? null);
|
|
631
|
+
}
|
|
632
|
+
setSelectedRequestId(currentRequests[newIdx]?.id ?? null);
|
|
599
633
|
}
|
|
600
634
|
}
|
|
601
635
|
else if (input === "u" && !key.ctrl) {
|
|
@@ -674,27 +708,6 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
674
708
|
const hasSelectedRequest = selectedFullRequest !== null;
|
|
675
709
|
const listWidth = hasSelectedRequest ? Math.floor(columns * listWidthRatio) : columns;
|
|
676
710
|
const accordionWidth = columns - listWidth;
|
|
677
|
-
// Status bar takes 2 rows (border line + content line), InfoBar takes 1 row, filter bar takes 2 rows when visible
|
|
678
|
-
const filterBarHeight = showFilter ? 2 : 0;
|
|
679
|
-
const infoBarHeight = 1;
|
|
680
|
-
const contentHeight = rows - 2 - infoBarHeight - filterBarHeight;
|
|
681
|
-
// Keep selection in bounds when requests change
|
|
682
|
-
React.useEffect(() => {
|
|
683
|
-
if (selectedIndex >= requests.length && requests.length > 0) {
|
|
684
|
-
setSelectedIndex(requests.length - 1);
|
|
685
|
-
}
|
|
686
|
-
}, [requests.length, selectedIndex]);
|
|
687
|
-
// Auto-scroll list view when selection moves outside visible area
|
|
688
|
-
React.useEffect(() => {
|
|
689
|
-
const visibleHeight = Math.max(1, contentHeight - 2);
|
|
690
|
-
if (selectedIndex < listScrollOffset) {
|
|
691
|
-
setListScrollOffset(selectedIndex);
|
|
692
|
-
}
|
|
693
|
-
else if (selectedIndex >= listScrollOffset + visibleHeight) {
|
|
694
|
-
setListScrollOffset(selectedIndex - visibleHeight + 1);
|
|
695
|
-
}
|
|
696
|
-
// Intentionally omit listScrollOffset to allow free mouse wheel scrolling
|
|
697
|
-
}, [selectedIndex, contentHeight]);
|
|
698
711
|
// Terminal size check — re-evaluates on resize via useStdoutDimensions
|
|
699
712
|
if (columns < MIN_TERMINAL_COLUMNS || rows < MIN_TERMINAL_ROWS) {
|
|
700
713
|
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows, width: columns, children: [_jsx(Text, { color: "red", bold: true, children: "Terminal too small" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Current: ", columns, "x", rows] }), _jsxs(Text, { children: ["Required: ", MIN_TERMINAL_COLUMNS, "x", MIN_TERMINAL_ROWS] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Please resize your terminal." })] }));
|
|
@@ -745,7 +758,7 @@ function AppContent({ __testEnableInput, projectRoot }) {
|
|
|
745
758
|
if (showTextViewer && textViewerData) {
|
|
746
759
|
return (_jsx(TextViewerModal, { text: textViewerData.text, title: textViewerData.title, contentType: textViewerData.contentType, bodySize: textViewerData.bodySize, width: columns, height: rows, onClose: () => setShowTextViewer(false), onStatus: showStatus, isActive: __testEnableInput || isRawModeSupported === true }));
|
|
747
760
|
}
|
|
748
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "row", height: contentHeight, children: [_jsx(RequestList, { ref: listPanelRef, requests: requests, selectedIndex: selectedIndex, isActive: activePanel === "list", isHovered: hoveredPanel === "list", width: listWidth, height: contentHeight, showFullUrl: showFullUrl, onItemClick: handleItemClick, scrollOffset:
|
|
761
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "row", height: contentHeight, children: [_jsx(RequestList, { ref: listPanelRef, requests: requests, selectedIndex: selectedIndex, isActive: activePanel === "list", isHovered: hoveredPanel === "list", width: listWidth, height: contentHeight, showFullUrl: showFullUrl, onItemClick: handleItemClick, scrollOffset: effectiveListScrollOffset, searchTerm: bodySearch ? undefined : filter.search, following: following, pendingNewCount: pendingNewCount }), hasSelectedRequest && (_jsx(AccordionPanel, { ref: accordionPanelRef, request: selectedFullRequest, isActive: activePanel === "accordion", width: accordionWidth, height: contentHeight, focusedSection: focusedSection, expandedSections: expandedSections }))] }), showFilter && (_jsx(FilterBar, { isActive: (__testEnableInput || isRawModeSupported === true) && showFilter, filter: filter, bodySearch: bodySearch, onFilterChange: handleFilterChange, onBodySearchChange: handleBodySearchChange, onClose: () => setShowFilter(false), onCancel: handleFilterCancel, width: columns })), _jsx(InfoBar, { interceptorErrorCount: interceptorEvents.counts.error, requestCount: requests.length, interceptorCount: interceptorEvents.interceptorCount, startTime: startTime, width: columns }), _jsx(StatusBar, { message: statusMessage, filterActive: isFilterActive(filter) || bodySearch !== undefined, filterOpen: showFilter, following: following, hasSelection: selectedFullRequest !== null, hasRequests: requests.length > 0, onViewableBodySection: currentBodyIsExportable && !currentBodyIsBinary, interceptorCount: interceptorEvents.interceptorCount, interceptorErrorCount: interceptorEvents.counts.error })] }));
|
|
749
762
|
}
|
|
750
763
|
export function App(props) {
|
|
751
764
|
return (_jsx(MouseProvider, { children: _jsx(AppContent, { ...props }) }));
|